From 09bcbb399e6441eaedf196ca4a50793196f67941 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 11:49:32 +0300 Subject: [PATCH 001/120] Fix balance calc --- apps/user/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/user/models.py b/apps/user/models.py index 43eac1cd..e0b7afa8 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -1,3 +1,6 @@ +from json import dumps +from rest_framework.authtoken.models import Token + from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver @@ -5,10 +8,8 @@ from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.postgres import fields as pgfields from django.utils.translation import gettext_lazy as _ -from rest_framework.authtoken.models import Token - -from json import dumps from api.v1 import serializers +from apps.payment.models import AuthorBalance class User(AbstractUser): @@ -63,7 +64,7 @@ class User(AbstractUser): @property def balance(self): - aggregate = self.balances.aggregate( + aggregate = self.balances.filter(type=AuthorBalance.IN).aggregate( models.Sum('amount'), models.Sum('commission'), ) From 6c60d411b4223026cf6188c053d375597d3240f5 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 12:08:53 +0300 Subject: [PATCH 002/120] Fix card field --- apps/user/models.py | 3 +-- apps/user/views.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/user/models.py b/apps/user/models.py index e0b7afa8..eb8bf8a3 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -9,7 +9,6 @@ from django.contrib.postgres import fields as pgfields from django.utils.translation import gettext_lazy as _ from api.v1 import serializers -from apps.payment.models import AuthorBalance class User(AbstractUser): @@ -64,7 +63,7 @@ class User(AbstractUser): @property def balance(self): - aggregate = self.balances.filter(type=AuthorBalance.IN).aggregate( + aggregate = self.balances.filter(type=0).aggregate( models.Sum('amount'), models.Sum('commission'), ) diff --git a/apps/user/views.py b/apps/user/views.py index 69e42bef..44d9a486 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -115,7 +115,7 @@ class PaymentHistoryView(FormView): type=AuthorBalance.OUT, amount=form.cleaned_data['amount'], status=AuthorBalance.PENDING, - card=form.cleaned_data['amount'], + card=form.cleaned_data['card'], ) return self.form_valid(form) else: From b8e393f82f1e7826b25f7557b500f9e7ca13c732 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 12:39:05 +0300 Subject: [PATCH 003/120] LIL-284. Add api for moderation of comments --- api/v1/serializers/course.py | 46 ++++++++++++++++++++++++++++++++++-- api/v1/urls.py | 4 +++- api/v1/views.py | 14 ++++++++++- 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 8139d8fa..52e16a25 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -1,7 +1,11 @@ from rest_framework import serializers -from apps.course.models import Category, Course, Material, Lesson, Like - +from apps.course.models import ( + Category, Course, + Comment, CourseComment, LessonComment, + Material, Lesson, + Like, +) from .content import ( ImageObjectSerializer, ContentSerializer, ContentCreateSerializer, GallerySerializer, GalleryImageSerializer, @@ -368,3 +372,41 @@ class CourseSerializer(serializers.ModelSerializer): 'update_at', 'deactivated_at', ) + + +class CommentSerializer(serializers.ModelSerializer): + + class Meta: + model = Comment + fields = ( + 'id', + 'content', + 'author', + 'parent', + 'created_at', + 'update_at', + ) + + def to_representation(self, instance): + if isinstance(instance, CourseComment): + return CourseCommentSerializer(instance, context=self.context).to_representation(instance) + elif isinstance(instance, LessonComment): + return LessonCommentSerializer(instance, context=self.context).to_representation(instance) + + +class CourseCommentSerializer(serializers.ModelSerializer): + + class Meta: + model = CourseComment + fields = CommentSerializer.Meta.fields + ( + 'course', + ) + + +class LessonCommentSerializer(serializers.ModelSerializer): + + class Meta: + model = LessonComment + fields = CommentSerializer.Meta.fields + ( + 'lesson', + ) diff --git a/api/v1/urls.py b/api/v1/urls.py index ab852c4d..3f487bd9 100644 --- a/api/v1/urls.py +++ b/api/v1/urls.py @@ -10,6 +10,7 @@ from .auth import ObtainToken from .views import ( AuthorBalanceViewSet, ConfigViewSet, CategoryViewSet, CourseViewSet, + CommentViewSet, MaterialViewSet, LikeViewSet, ImageViewSet, TextViewSet, ImageTextViewSet, VideoViewSet, @@ -20,8 +21,9 @@ from .views import ( router = DefaultRouter() router.register(r'author-balance', AuthorBalanceViewSet, base_name='author-balance') -router.register(r'courses', CourseViewSet, base_name='courses') router.register(r'categories', CategoryViewSet, base_name='categories') +router.register(r'courses', CourseViewSet, base_name='courses') +router.register(r'comments', CommentViewSet, base_name='comments') router.register(r'materials', MaterialViewSet, base_name='materials') router.register(r'lessons', LessonViewSet, base_name='lessons') router.register(r'likes', LikeViewSet, base_name='likes') diff --git a/api/v1/views.py b/api/v1/views.py index 68f0f428..6ae66284 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -15,6 +15,7 @@ from .serializers.course import ( CategorySerializer, LikeSerializer, CourseSerializer, CourseCreateSerializer, CourseBulkChangeCategorySerializer, + CommentSerializer, MaterialSerializer, MaterialCreateSerializer, LessonSerializer, LessonCreateSerializer, ) @@ -35,7 +36,12 @@ from .serializers.user import ( from .permissions import IsAdmin, IsAdminOrIsSelf, IsAuthorOrAdmin, IsAuthorObjectOrAdmin -from apps.course.models import Category, Course, Material, Lesson, Like +from apps.course.models import ( + Category, Course, + Comment, CourseComment, LessonComment, + Material, Lesson, + Like, +) from apps.content.models import ( Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, @@ -329,3 +335,9 @@ class ConfigViewSet(generics.RetrieveUpdateAPIView): if serializer.is_valid(): serializer.update(get_values(), serializer.validated_data) return Response(serializer.data) + + +class CommentViewSet(ExtendedModelViewSet): + queryset = Comment.objects.all() + serializer_class = CommentSerializer + permission_classes = (IsAdmin,) From 5b95f099497d1269c255a4dc66505d2b64bcf4d8 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 13:06:43 +0300 Subject: [PATCH 004/120] LIL-33. Add DeactivatedMixin to Comment model --- .../migrations/0035_comment_deactivated_at.py | 18 ++++++++++++++++++ apps/course/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 apps/course/migrations/0035_comment_deactivated_at.py diff --git a/apps/course/migrations/0035_comment_deactivated_at.py b/apps/course/migrations/0035_comment_deactivated_at.py new file mode 100644 index 00000000..cd5ba08e --- /dev/null +++ b/apps/course/migrations/0035_comment_deactivated_at.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-03-12 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('course', '0034_auto_20180215_1503'), + ] + + operations = [ + migrations.AddField( + model_name='comment', + name='deactivated_at', + field=models.DateTimeField(blank=True, default=None, null=True), + ), + ] diff --git a/apps/course/models.py b/apps/course/models.py index ad59b1b1..98a64423 100644 --- a/apps/course/models.py +++ b/apps/course/models.py @@ -196,7 +196,7 @@ class Material(models.Model): ordering = ('title',) -class Comment(PolymorphicMPTTModel): +class Comment(PolymorphicMPTTModel, DeactivatedMixin): content = models.TextField('Текст комментария', default='') author = models.ForeignKey(User, on_delete=models.CASCADE) parent = PolymorphicTreeForeignKey( From ff8708ea0073743bba6e029ad82636fde17f943a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 13:11:15 +0300 Subject: [PATCH 005/120] LIL-33. Add deactivated_at field to CommentSerializer --- api/v1/serializers/course.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 52e16a25..a9679fee 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -383,6 +383,7 @@ class CommentSerializer(serializers.ModelSerializer): 'content', 'author', 'parent', + 'deactivated_at', 'created_at', 'update_at', ) From 7b7ee5230caa4e76a2dd8687d8bd26e8452c099f Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 13:11:41 +0300 Subject: [PATCH 006/120] LIL-33. Don't show deactivated comments. --- apps/course/templates/course/blocks/comment.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/course/templates/course/blocks/comment.html b/apps/course/templates/course/blocks/comment.html index 1f338358..463536af 100644 --- a/apps/course/templates/course/blocks/comment.html +++ b/apps/course/templates/course/blocks/comment.html @@ -1,5 +1,5 @@ {% load static %} - +{% if not node.deactivated_at %}
{% if node.author.photo %}
@@ -24,4 +24,5 @@ {% endif %}
- \ No newline at end of file + +{% endif %} From d10369147e4e269088c6cfca849fb29edd19fd17 Mon Sep 17 00:00:00 2001 From: Sanasol Date: Mon, 12 Mar 2018 17:12:50 +0700 Subject: [PATCH 007/120] fixes, gallery --- apps/course/templates/course/content/gallery.html | 12 ++++++++---- apps/course/templates/course/content/image.html | 8 +++++--- apps/course/templates/course/content/imagetext.html | 6 ++++-- web/package.json | 1 + web/src/components/CourseRedactor.vue | 2 +- web/src/js/modules/common.js | 6 ++++++ web/src/sass/_common.sass | 2 +- web/src/sass/app.sass | 1 + 8 files changed, 27 insertions(+), 11 deletions(-) diff --git a/apps/course/templates/course/content/gallery.html b/apps/course/templates/course/content/gallery.html index 771df8b6..32a4bf91 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/course/templates/course/content/gallery.html @@ -1,18 +1,22 @@ {% if results %}
Галерея итогов обучения
-
+ {% else %}
{{ content.title }}
-
+ diff --git a/apps/course/templates/course/content/image.html b/apps/course/templates/course/content/image.html index 5daeba4e..1117d434 100644 --- a/apps/course/templates/course/content/image.html +++ b/apps/course/templates/course/content/image.html @@ -1,6 +1,8 @@
{{ content.title }}
-
- -
\ No newline at end of file + diff --git a/apps/course/templates/course/content/imagetext.html b/apps/course/templates/course/content/imagetext.html index 9849e357..0b3301cc 100644 --- a/apps/course/templates/course/content/imagetext.html +++ b/apps/course/templates/course/content/imagetext.html @@ -4,6 +4,8 @@
{{ content.txt | safe }}
-
- + \ No newline at end of file diff --git a/web/package.json b/web/package.json index 96794b84..ba9de0b4 100755 --- a/web/package.json +++ b/web/package.json @@ -55,6 +55,7 @@ "dependencies": { "axios": "^0.17.1", "babel-polyfill": "^6.26.0", + "baguettebox.js": "^1.10.0", "history": "^4.7.2", "ilyabirman-likely": "^2.3.0", "inputmask": "^3.3.11", diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index 0f94eb3e..d173f8cf 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -551,7 +551,7 @@ clearTimeout(this.savingTimeout); document.getElementById('course-redactor__saving-status').innerText = 'СОХРАНЕНИЕ'; const courseObject = this.course; - courseObject.url = slugify(courseObject.url); + courseObject.url = (courseObject.url) ? slugify(courseObject.url):courseObject.url; api.saveCourse(courseObject, this.accessToken) .then((response) => { this.courseSaving = false; diff --git a/web/src/js/modules/common.js b/web/src/js/modules/common.js index ecbd4dee..dea36989 100644 --- a/web/src/js/modules/common.js +++ b/web/src/js/modules/common.js @@ -1,8 +1,14 @@ import $ from 'jquery'; import Inputmask from "inputmask"; import SmoothScroll from 'smooth-scroll/dist/js/smooth-scroll'; +import baguetteBox from 'baguettebox.js' + window.Inputmask = Inputmask; +window.baguetteBox = baguetteBox; + $(document).ready(function () { + + baguetteBox.run('.gallery'); // Добавляем заголовок X-CSRFToken для всех AJAX запросов JQuery. $.ajaxSetup({ headers: { diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index de962138..4d66e5d8 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -1857,7 +1857,7 @@ a.grey-link left: -10px right: -10px height: 2px - margin-top: -2px + margin-top: 5px background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) &__title display: table diff --git a/web/src/sass/app.sass b/web/src/sass/app.sass index 9272f307..020d748e 100755 --- a/web/src/sass/app.sass +++ b/web/src/sass/app.sass @@ -2,3 +2,4 @@ @import helpers/all @import generated/sprite-svg @import common +@import '../../node_modules/baguettebox.js/src/baguetteBox.scss' From 9e26bc15c5f06e933f618389bea6e3a22ab2a558 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 13:31:39 +0300 Subject: [PATCH 008/120] LIL-284. Comment author as object --- api/v1/serializers/course.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index a9679fee..67516670 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -375,6 +375,7 @@ class CourseSerializer(serializers.ModelSerializer): class CommentSerializer(serializers.ModelSerializer): + author = UserSerializer() class Meta: model = Comment @@ -396,6 +397,7 @@ class CommentSerializer(serializers.ModelSerializer): class CourseCommentSerializer(serializers.ModelSerializer): + author = UserSerializer() class Meta: model = CourseComment @@ -405,6 +407,7 @@ class CourseCommentSerializer(serializers.ModelSerializer): class LessonCommentSerializer(serializers.ModelSerializer): + author = UserSerializer() class Meta: model = LessonComment From f3638fafb0c7f44a64c9e201d7e985a1ad5027a3 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 17:02:05 +0300 Subject: [PATCH 009/120] LIL-290. Add AuthorRequest model --- apps/user/migrations/0009_authorrequest.py | 27 ++++++++++++++++ apps/user/models.py | 37 ++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 apps/user/migrations/0009_authorrequest.py diff --git a/apps/user/migrations/0009_authorrequest.py b/apps/user/migrations/0009_authorrequest.py new file mode 100644 index 00000000..9e10d01c --- /dev/null +++ b/apps/user/migrations/0009_authorrequest.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.2 on 2018-03-12 14:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0008_auto_20180212_0750'), + ] + + operations = [ + migrations.CreateModel( + name='AuthorRequest', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=30, verbose_name='first name')), + ('last_name', models.CharField(max_length=150, verbose_name='last name')), + ('email', models.EmailField(max_length=254, verbose_name='email address')), + ('about', models.CharField(blank=True, max_length=1000, null=True, verbose_name='О себе')), + ('facebook', models.URLField(blank=True, default='', null=True)), + ('status', models.PositiveSmallIntegerField(choices=[(0, 'pending'), (1, 'accepted'), (2, 'declined')], default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('update_at', models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index eb8bf8a3..a543d782 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -85,3 +85,40 @@ def create_auth_token(sender, instance=None, created=False, **kwargs): instance.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE] ) and hasattr(instance, 'auth_token'): instance.auth_token.delete() + + +class AuthorRequestManager(models.Manager): + def create_by_user(self, user): + obj = self.model( + first_name=user.first_name, + last_name=user.last_name, + email=user.email, + about=user.about, + facebook=user.facebook, + ) + self._for_write = True + obj.save(force_insert=True, using=self.db) + return obj + + +class AuthorRequest(models.Model): + PENDING = 0 + ACCEPTED = 1 + DECLINED = 2 + STATUS_CHOICES = ( + (PENDING, 'pending'), + (ACCEPTED, 'accepted'), + (DECLINED, 'declined'), + ) + + first_name = models.CharField(_('first name'), max_length=30) + last_name = models.CharField(_('last name'), max_length=150) + email = models.EmailField(_('email address')) + about = models.CharField('О себе', max_length=1000, null=True, blank=True) + facebook = models.URLField(default='', null=True, blank=True) + status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=PENDING) + + created_at = models.DateTimeField(auto_now_add=True) + update_at = models.DateTimeField(auto_now=True) + + objects = AuthorRequestManager() From 77ca5e3737ae82766e331d7ee038aca59c8f2766 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 17:06:05 +0300 Subject: [PATCH 010/120] LIL-291. Add author reques template & form --- apps/user/forms.py | 8 +++ apps/user/templates/user/become-author.html | 61 +++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 apps/user/templates/user/become-author.html diff --git a/apps/user/forms.py b/apps/user/forms.py index 2f75ef68..f2933c70 100644 --- a/apps/user/forms.py +++ b/apps/user/forms.py @@ -54,3 +54,11 @@ class UserEditForm(forms.ModelForm): class WithdrawalForm(forms.Form): amount = forms.DecimalField(required=True, min_value=2000) card = CreditCardField(required=True) + + +class AuthorRequesForm(forms.Form): + first_name = forms.CharField() + last_name = forms.CharField() + email = forms.CharField() + about = forms.CharField() + facebook = forms.URLField(required=False) diff --git a/apps/user/templates/user/become-author.html b/apps/user/templates/user/become-author.html new file mode 100644 index 00000000..bc08345c --- /dev/null +++ b/apps/user/templates/user/become-author.html @@ -0,0 +1,61 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} +
+
+
{% csrf_token %} +
+
Стать автором
+
+
+
ИМЯ
+
+ +
+ {% if form.first_name.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
ФАМИЛИЯ
+
+ +
+ {% if form.last_name.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
+
Почта
+
+ +
+ {% if form.email.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
О себе
+
+ +
+ {% if form.about.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
FACEBOOK
+
+ +
+ {% if form.facebook.errors %} +
Укажите корректно свои данные
+ {% endif %} +
+
+
+ +
+
+
+
+{% endblock content %} From a93aa10474ecad54c41ab5dc0575d521f67d5a1a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 18:56:02 +0300 Subject: [PATCH 011/120] LIL-300. Add success page for author request --- apps/user/templates/user/become-author-success.html | 12 ++++++++++++ project/urls.py | 1 + 2 files changed, 13 insertions(+) create mode 100644 apps/user/templates/user/become-author-success.html diff --git a/apps/user/templates/user/become-author-success.html b/apps/user/templates/user/become-author-success.html new file mode 100644 index 00000000..e6e97f7d --- /dev/null +++ b/apps/user/templates/user/become-author-success.html @@ -0,0 +1,12 @@ +{% extends "templates/lilcity/index.html" %} {% load static %} {% block content %} +
+
+
+
Ваша заявка отправлена!
+ +
+
+
+{% endblock content %} diff --git a/project/urls.py b/project/urls.py index 30a14cbd..52755963 100644 --- a/project/urls.py +++ b/project/urls.py @@ -35,6 +35,7 @@ from .views import IndexView urlpatterns = [ path('admin/', admin.site.urls), path('auth/', include(('apps.auth.urls', 'lilcity'))), + path('author-request/success/', TemplateView.as_view(template_name='user/become-author-success.html'), name='author-request-success'), path('courses/', CoursesView.as_view(), name='courses'), path('course/create', CourseEditView.as_view(), name='course_create'), path('course/on-moderation', CourseOnModerationView.as_view(), name='course-on-moderation'), From e68b383d57541a9e2bae458c415dae0a00b8a9e8 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 19:11:25 +0300 Subject: [PATCH 012/120] LIL-290. Add unique to AuthorRequest email field --- .../user/migrations/0010_auto_20180312_1610.py | 18 ++++++++++++++++++ apps/user/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 apps/user/migrations/0010_auto_20180312_1610.py diff --git a/apps/user/migrations/0010_auto_20180312_1610.py b/apps/user/migrations/0010_auto_20180312_1610.py new file mode 100644 index 00000000..b691deea --- /dev/null +++ b/apps/user/migrations/0010_auto_20180312_1610.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.2 on 2018-03-12 16:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0009_authorrequest'), + ] + + operations = [ + migrations.AlterField( + model_name='authorrequest', + name='email', + field=models.EmailField(max_length=254, unique=True, verbose_name='email address'), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index a543d782..9ce0e760 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -113,7 +113,7 @@ class AuthorRequest(models.Model): first_name = models.CharField(_('first name'), max_length=30) last_name = models.CharField(_('last name'), max_length=150) - email = models.EmailField(_('email address')) + email = models.EmailField(_('email address'), unique=True) about = models.CharField('О себе', max_length=1000, null=True, blank=True) facebook = models.URLField(default='', null=True, blank=True) status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES, default=PENDING) From d5f65f5b957245c648c86631bfd7eaca39847396 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 12 Mar 2018 19:12:47 +0300 Subject: [PATCH 013/120] LIL-299. Add AuthorRequest view --- .../user/templates/user/profile-settings.html | 4 +- apps/user/views.py | 46 ++++++++++++++++++- project/templates/lilcity/index.html | 7 ++- project/urls.py | 12 +++-- 4 files changed, 59 insertions(+), 10 deletions(-) diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html index 68d41375..33baee8e 100644 --- a/apps/user/templates/user/profile-settings.html +++ b/apps/user/templates/user/profile-settings.html @@ -79,7 +79,7 @@ {% if form.email.errors %}
Укажите корректно свои данные
{% endif %} -
+
ГОРОД
@@ -250,4 +250,4 @@ var openFile = function(file) { reader.readAsDataURL(input.files[0]); }; -{% endblock content %} \ No newline at end of file +{% endblock content %} diff --git a/apps/user/views.py b/apps/user/views.py index 44d9a486..53e1d689 100644 --- a/apps/user/views.py +++ b/apps/user/views.py @@ -26,7 +26,8 @@ from apps.notification.utils import send_email from apps.school.models import SchoolSchedule from apps.payment.models import AuthorBalance, CoursePayment, SchoolPayment -from .forms import UserEditForm, WithdrawalForm +from .forms import AuthorRequesForm, UserEditForm, WithdrawalForm +from .models import AuthorRequest User = get_user_model() @@ -182,3 +183,46 @@ class UserEditView(UpdateView): def get_success_url(self): return reverse('user-edit-profile', args=[self.object.id]) + + +class AuthorRequestView(FormView): + template_name = 'user/become-author.html' + form_class = AuthorRequesForm + success_url = reverse_lazy('author-request-success') + + def post(self, request, pk=None): + form = self.get_form() + if form.is_valid(): + if request.user.is_authenticated: + email = request.user.email + if request.user.role in [User.AUTHOR_ROLE, User.ADMIN_ROLE]: + messages.info('Вы уже являетесь автором') + return self.form_invalid(form) + else: + email = form.cleaned_data['email'] + + if AuthorRequest.objects.filter(email=email).exists(): + messages.error('Вы уже отправили заявку на преподавателя') + return self.form_invalid(form) + + AuthorRequest.objects.create( + first_name=form.cleaned_daLILta['first_name'], + last_name=form.cleaned_data['last_name'], + email=email, + about=form.cleaned_data['about'], + facebook=form.cleaned_data['facebook'], + ) + return self.form_valid(form) + else: + return self.form_invalid(form) + + def get_context_data(self): + if self.request.user.is_authenticated: + self.initial = { + 'first_name': self.request.user.first_name, + 'last_name': self.request.user.last_name, + 'email': self.request.user.email, + 'about': self.request.user.about, + 'facebook': self.request.user.facebook, + } + return super().get_context_data() diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 2f28628d..b6bb1808 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -188,8 +188,11 @@
{% if course_items %} diff --git a/project/templates/lilcity/school_schedules.html b/project/templates/lilcity/school_schedules.html new file mode 100644 index 00000000..88601188 --- /dev/null +++ b/project/templates/lilcity/school_schedules.html @@ -0,0 +1,52 @@ +{% load static %} + + + + + + + + + + + +
+
+ +
Расписание
+
+
+ {% for school_schedule in school_schedules %} +
+
{{ school_schedule }}
+
+
{{ school_schedule.title }}
+
{{ school_schedule.description }}
+
+ +
{{ school_schedule.materials }}
+
+
+
+ {% endfor %} +
+ {% comment %}
+ Распечатать расписание чтобы не забыть +
{% endcomment %} +
+
+ + + + diff --git a/project/urls.py b/project/urls.py index b7f3dde3..b0c6ddb8 100644 --- a/project/urls.py +++ b/project/urls.py @@ -36,7 +36,7 @@ from apps.payment.views import ( SchoolBuyView, ) -from .views import IndexView +from .views import IndexView, SchoolSchedulesView urlpatterns = [ path('admin/', admin.site.urls), @@ -71,6 +71,7 @@ urlpatterns = [ path('privacy', TemplateView.as_view(template_name='templates/lilcity/privacy_policy.html'), name='privacy'), path('terms', TemplateView.as_view(template_name='templates/lilcity/terms.html'), name='terms'), path('refund-policy', TemplateView.as_view(template_name='templates/lilcity/refund_policy.html'), name='refund_policy'), + path('school-schedules', SchoolSchedulesView.as_view(), name='school_schedules'), path('', IndexView.as_view(), name='index'), path('api/v1/', include(('api.v1.urls', 'api_v1'))), path('test', TemplateView.as_view(template_name='templates/lilcity/test.html'), name='test'), diff --git a/project/views.py b/project/views.py index 9dafb0a4..fcedb726 100644 --- a/project/views.py +++ b/project/views.py @@ -14,3 +14,12 @@ class IndexView(TemplateView): 'school_schedules': SchoolSchedule.objects.all(), }) return context + + +class SchoolSchedulesView(TemplateView): + template_name = 'templates/lilcity/school_schedules.html' + + def get_context_data(self): + context = super().get_context_data() + context['school_schedules'] = SchoolSchedule.objects.all() + return context From 7fa8a6ca4600617c8e4736cca3f9e12d0e60c830 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Thu, 15 Mar 2018 14:06:49 +0300 Subject: [PATCH 048/120] LIL-315. Fix user icon in comment block --- apps/course/templates/course/course.html | 4 ++-- apps/course/templates/course/lesson.html | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 5f7a00c8..a2787322 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -409,11 +409,11 @@
Задавайте вопросы:
- {% if user.is_authenticated %} + {% if request.user.is_authenticated %}
- +
В ответ на diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index 9286f106..abd108b2 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -33,7 +33,7 @@ {% else %} - {% endif %} + {% endif %} @@ -97,10 +97,10 @@
Задавайте вопросы:
- {% if user.is_authenticated %} + {% if request.user.is_authenticated %}
- +
From 9416e3e978243f12239de345a610d075a193c725 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Thu, 15 Mar 2018 14:24:47 +0300 Subject: [PATCH 049/120] Fix user photo if not found --- apps/course/templates/course/course.html | 9 ++++++++- apps/course/templates/course/lesson.html | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index a2787322..4426225e 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -413,7 +413,14 @@
- +
В ответ на diff --git a/apps/course/templates/course/lesson.html b/apps/course/templates/course/lesson.html index abd108b2..480d7d33 100644 --- a/apps/course/templates/course/lesson.html +++ b/apps/course/templates/course/lesson.html @@ -100,7 +100,14 @@ {% if request.user.is_authenticated %}
- +
From 8fc40c43d3b6194fcadf954980349eaa3a85c7d6 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Thu, 15 Mar 2018 15:13:46 +0300 Subject: [PATCH 050/120] Fix cource user perms --- apps/course/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/course/views.py b/apps/course/views.py index 68fd1b1a..caa3bd5f 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -185,7 +185,7 @@ class CourseView(DetailView): def get(self, request, *args, **kwargs): response = super().get(request, *args, **kwargs) context = self.get_context_data() - if not request.user.is_authenticated or (not request.user.is_authenticated and self.object.status != Course.PUBLISHED) or\ + if (not request.user.is_authenticated and self.object.status != Course.PUBLISHED) or\ (request.user.is_authenticated and request.user.role not in [User.AUTHOR_ROLE, User.ADMIN_ROLE] and self.object.author != request.user and self.only_lessons and not context['paid']): raise Http404 return response From df7a99168d8f6005073553093a8c8937786ddb6d Mon Sep 17 00:00:00 2001 From: Sanasol Date: Thu, 15 Mar 2018 19:25:41 +0700 Subject: [PATCH 051/120] style import fix --- web/src/sass/app.sass | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/sass/app.sass b/web/src/sass/app.sass index 020d748e..82cdc94a 100755 --- a/web/src/sass/app.sass +++ b/web/src/sass/app.sass @@ -2,4 +2,4 @@ @import helpers/all @import generated/sprite-svg @import common -@import '../../node_modules/baguettebox.js/src/baguetteBox.scss' +@import '~baguettebox.js/dist/baguetteBox.min.css'; \ No newline at end of file From acef8ab30a5b5528584c5ec380d41d72fac9b3b3 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Thu, 15 Mar 2018 20:31:25 +0300 Subject: [PATCH 052/120] LIL-320. Add phone to user model --- api/v1/serializers/user.py | 6 +++++- apps/user/forms.py | 3 +++ apps/user/migrations/0018_user_phone.py | 19 +++++++++++++++++++ apps/user/models.py | 3 +++ apps/user/tasks.py | 3 ++- .../user/templates/user/profile-settings.html | 9 +++++++++ requirements.txt | 1 + 7 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 apps/user/migrations/0018_user_phone.py diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index d548da19..fe316cf5 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -1,6 +1,8 @@ -from django.contrib.auth import get_user_model +from phonenumber_field.serializerfields import PhoneNumberField from rest_framework import serializers +from django.contrib.auth import get_user_model + from . import Base64ImageField from apps.user.models import AuthorRequest @@ -8,6 +10,7 @@ User = get_user_model() class UserSerializer(serializers.ModelSerializer): + phone = PhoneNumberField() class Meta: model = User @@ -15,6 +18,7 @@ class UserSerializer(serializers.ModelSerializer): 'id', 'username', 'email', + 'phone', 'first_name', 'last_name', 'is_staff', diff --git a/apps/user/forms.py b/apps/user/forms.py index f2933c70..2445d522 100644 --- a/apps/user/forms.py +++ b/apps/user/forms.py @@ -1,5 +1,6 @@ from django import forms from django.contrib.auth import get_user_model +from phonenumber_field.formfields import PhoneNumberField from .fields import CreditCardField @@ -10,6 +11,7 @@ class UserEditForm(forms.ModelForm): # first_name = forms.CharField() # last_name = forms.CharField() # email = forms.CharField() + phone = PhoneNumberField() # city = forms.CharField() # country = forms.CharField() birthday = forms.DateField(input_formats=['%d.%m.%Y'], required=False) @@ -33,6 +35,7 @@ class UserEditForm(forms.ModelForm): 'first_name', 'last_name', 'email', + 'phone', 'city', 'country', 'birthday', diff --git a/apps/user/migrations/0018_user_phone.py b/apps/user/migrations/0018_user_phone.py new file mode 100644 index 00000000..d377bcf5 --- /dev/null +++ b/apps/user/migrations/0018_user_phone.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.3 on 2018-03-15 17:19 + +from django.db import migrations +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0017_subscriptioncategory_auto_add'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='phone', + field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, unique=True), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index f4c56536..57ec8cd2 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -1,5 +1,6 @@ from json import dumps from rest_framework.authtoken.models import Token +from phonenumber_field.modelfields import PhoneNumberField from django.db import models from django.db.models.signals import post_save @@ -33,6 +34,7 @@ class User(AbstractUser): (FEMALE, 'Женщина'), ) email = models.EmailField(_('email address'), unique=True) + phone = PhoneNumberField(null=True, blank=True, unique=True) role = models.PositiveSmallIntegerField( 'Роль', default=0, choices=ROLE_CHOICES) gender = models.CharField( @@ -96,6 +98,7 @@ def send_user_info_to_mixpanel(sender, instance=None, created=False, **kwargs): user_to_mixpanel.delay( instance.id, instance.email, + str(instance.phone), instance.first_name, instance.last_name, instance.date_joined, diff --git a/apps/user/tasks.py b/apps/user/tasks.py index 702c804e..a95988be 100644 --- a/apps/user/tasks.py +++ b/apps/user/tasks.py @@ -6,12 +6,13 @@ from project.celery import app @app.task -def user_to_mixpanel(user_id, email, first_name, last_name, date_joined, role, subscriptions): +def user_to_mixpanel(user_id, email, phone, first_name, last_name, date_joined, role, subscriptions): mix = Mixpanel(settings.MIX_TOKEN) mix.people_set( user_id, { '$email': email, + '$phone': phone, '$first_name': first_name, '$last_name': last_name, '$created': date_joined, diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html index 33baee8e..8eddef8c 100644 --- a/apps/user/templates/user/profile-settings.html +++ b/apps/user/templates/user/profile-settings.html @@ -80,6 +80,15 @@
Укажите корректно свои данные
{% endif %}
+
+
Телефон
+
+ +
+ {% if form.phone.errors %} +
Укажите корректно свои данные
+ {% endif %} +
ГОРОД
diff --git a/requirements.txt b/requirements.txt index 4faab0da..2ca79a88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ django-constance[database]==2.1.0 django-filter==2.0.0.dev1 django-mptt==0.9.0 django-silk==2.0.0 +django-phonenumber-field==2.0.0 django-polymorphic-tree==1.5 djangorestframework==3.7.7 drf-yasg[validation]==1.5.0 From 5dfed1f73daa68123432029d17517a7462515309 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 12:20:40 +0300 Subject: [PATCH 053/120] Fix course slug --- api/v1/serializers/course.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/serializers/course.py b/api/v1/serializers/course.py index 1c9efa96..7f45c7a2 100644 --- a/api/v1/serializers/course.py +++ b/api/v1/serializers/course.py @@ -88,7 +88,7 @@ class CourseCreateSerializer(DispatchContentMixin, ): title = serializers.CharField(allow_blank=True) short_description = serializers.CharField(allow_blank=True) - slug = serializers.SlugField(allow_unicode=True, allow_blank=True, required=False) + slug = serializers.SlugField(allow_unicode=True, allow_blank=True, allow_null=True, required=False) content = serializers.ListSerializer( child=ContentCreateSerializer(), required=False, From ed7e888b86041f485e6a46e01db215b35c8c8572 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 12:58:29 +0300 Subject: [PATCH 054/120] LIL-314. Fix gallery images --- apps/course/templates/course/content/gallery.html | 12 ++++++++---- project/settings.py | 1 + requirements.txt | 3 ++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/course/templates/course/content/gallery.html b/apps/course/templates/course/content/gallery.html index 32a4bf91..fd0d1389 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/course/templates/course/content/gallery.html @@ -1,10 +1,13 @@ +{% load thumbnail %} {% if results %}
Галерея итогов обучения
-{% endif %} \ No newline at end of file +{% endif %} diff --git a/project/settings.py b/project/settings.py index a9e02e83..25bd6e03 100644 --- a/project/settings.py +++ b/project/settings.py @@ -52,6 +52,7 @@ INSTALLED_APPS = [ 'corsheaders', 'constance', 'constance.backends.database', + 'sorl.thumbnail', ] + [ 'apps.auth.apps', 'apps.user', diff --git a/requirements.txt b/requirements.txt index 2ca79a88..cf55c8a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,9 +16,10 @@ drf-yasg[validation]==1.5.0 facepy==1.0.9 gunicorn==19.7.1 mixpanel==4.3.2 -requests==2.18.4 psycopg2-binary==2.7.4 Pillow==5.0.0 +requests==2.18.4 +sorl-thumbnail==12.4.1 twilio==6.10.5 # paymentwall-python==1.0.7 git+https://github.com/ivlevdenis/paymentwall-python.git From f9c96c654c34ce2224c03701cddf56319fd2b3ae Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 13:15:53 +0300 Subject: [PATCH 055/120] Fix gallery template --- apps/course/templates/course/content/gallery.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/course/templates/course/content/gallery.html b/apps/course/templates/course/content/gallery.html index fd0d1389..0d751dc6 100644 --- a/apps/course/templates/course/content/gallery.html +++ b/apps/course/templates/course/content/gallery.html @@ -20,7 +20,8 @@ {% thumbnail image.img.image "140x140" crop="center" as im %} - {% endthumbnail %} + {% endthumbnail %} +
{% endfor %}
From cbfd47c77217b85e12c0da85d630f837c3c359d5 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 13:16:28 +0300 Subject: [PATCH 056/120] LIL-319. Fix course preview img --- apps/course/templates/course/_items.html | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index 260a482e..2fd55a31 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -1,3 +1,4 @@ +{% load thumbnail %} {% load static %} {% load data_liked from data_liked %} @@ -8,9 +9,11 @@ > {% if course.cover %} - + {% thumbnail course.cover.image "300x170" crop="center" as im %} + + {% endthumbnail %} {% else %} - + {% endif %}
Подробнее
{% if course.is_featured %} @@ -86,4 +89,4 @@
-
\ No newline at end of file +
From 4026f04044cd3aa434f0fe1c272f3dc93e23677b Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 13:22:24 +0300 Subject: [PATCH 057/120] Use empty tag for img thumbs --- apps/course/templates/course/_items.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/course/templates/course/_items.html b/apps/course/templates/course/_items.html index 2fd55a31..33312e53 100644 --- a/apps/course/templates/course/_items.html +++ b/apps/course/templates/course/_items.html @@ -8,13 +8,11 @@ {% if course.is_deferred_start %}data-future-course data-future-course-time={{ course.deferred_start_at.timestamp }}{% endif %} > - {% if course.cover %} {% thumbnail course.cover.image "300x170" crop="center" as im %} - {% endthumbnail %} - {% else %} + {% empty %} - {% endif %} + {% endthumbnail %}
Подробнее
{% if course.is_featured %}
From 3fcde043876d6951098ef6f566d89a0b00426838 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 14:07:08 +0300 Subject: [PATCH 058/120] LIL-294. Add uuid field to content block --- api/v1/serializers/content.py | 5 +++++ apps/content/migrations/0015_content_uuid.py | 18 ++++++++++++++++++ apps/content/models.py | 1 + 3 files changed, 24 insertions(+) create mode 100644 apps/content/migrations/0015_content_uuid.py diff --git a/api/v1/serializers/content.py b/api/v1/serializers/content.py index c01a2c65..e576912b 100644 --- a/api/v1/serializers/content.py +++ b/api/v1/serializers/content.py @@ -61,6 +61,7 @@ class ImageCreateSerializer(serializers.ModelSerializer): model = Image fields = ( 'id', + 'uuid', 'course', 'lesson', 'title', @@ -93,6 +94,7 @@ class TextCreateSerializer(serializers.ModelSerializer): model = Text fields = ( 'id', + 'uuid', 'course', 'lesson', 'title', @@ -124,6 +126,7 @@ class ImageTextCreateSerializer(serializers.ModelSerializer): model = ImageText fields = ( 'id', + 'uuid', 'course', 'lesson', 'title', @@ -157,6 +160,7 @@ class VideoCreateSerializer(serializers.ModelSerializer): model = Video fields = ( 'id', + 'uuid', 'course', 'lesson', 'title', @@ -212,6 +216,7 @@ class GallerySerializer(serializers.ModelSerializer): model = Gallery fields = ( 'id', + 'uuid', 'course', 'lesson', 'title', diff --git a/apps/content/migrations/0015_content_uuid.py b/apps/content/migrations/0015_content_uuid.py new file mode 100644 index 00000000..54a1997c --- /dev/null +++ b/apps/content/migrations/0015_content_uuid.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-16 11:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('content', '0014_auto_20180215_1503'), + ] + + operations = [ + migrations.AddField( + model_name='content', + name='uuid', + field=models.UUIDField(blank=True, null=True), + ), + ] diff --git a/apps/content/models.py b/apps/content/models.py index 345221f4..5c0b9b66 100644 --- a/apps/content/models.py +++ b/apps/content/models.py @@ -17,6 +17,7 @@ class ImageObject(models.Model): class Content(PolymorphicModel): + uuid = models.UUIDField(null=True, blank=True) course = models.ForeignKey( 'course.Course', on_delete=models.CASCADE, null=True, blank=True, From 75954748c20bb705ebc742aee77b80d74811692a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 14:19:28 +0300 Subject: [PATCH 059/120] LIL-308. Only authors & admins in author balance req --- api/v1/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/v1/views.py b/api/v1/views.py index 4a854b78..8aaf84b8 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -55,7 +55,9 @@ User = get_user_model() class AuthorBalanceViewSet(ExtendedModelViewSet): - queryset = AuthorBalance.objects.all() + queryset = AuthorBalance.objects.filter( + author__role__in=[User.AUTHOR_ROLE, User.ADMIN_ROLE], + ) serializer_class = AuthorBalanceCreateSerializer serializer_class_map = { 'list': AuthorBalanceSerializer, From 1f286028800bd5fd9e34f3309b28eee5b25a8264 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 15:27:02 +0300 Subject: [PATCH 060/120] LIL-312. Use fix school amount calc --- apps/payment/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/payment/models.py b/apps/payment/models.py index 5ec8e1ac..d3b42c8c 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -157,7 +157,7 @@ class SchoolPayment(Payment): models.Sum('month_price'), ) month_price_sum = aggregate.get('month_price__sum', 0) - if len(self.weekdays) > 7: + if month_price_sum > config.SERVICE_DISCOUNT_MIN_AMOUNT: discount = config.SERVICE_DISCOUNT else: discount = 0 From e63c7c076a508f14c2889a2d4b8e177e3c16dddc Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 15:38:55 +0300 Subject: [PATCH 061/120] LIL-317. Add SCHOOL_LOGO_IMAGE config field --- api/v1/serializers/config.py | 1 + project/settings.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index e17ce57e..31269d66 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -19,6 +19,7 @@ class ConfigSerializer(serializers.Serializer): INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) + SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) def to_representation(self, instance): ret = OrderedDict() diff --git a/project/settings.py b/project/settings.py index 25bd6e03..59b1f339 100644 --- a/project/settings.py +++ b/project/settings.py @@ -231,6 +231,9 @@ CELERY_BEAT_SCHEDULE = { # Dynamic settings CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' +CONSTANCE_ADDITIONAL_FIELDS = { + 'image_field': ['django.forms.ImageField', {}] +} CONSTANCE_CONFIG = OrderedDict(( ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')), ('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')), @@ -240,6 +243,7 @@ CONSTANCE_CONFIG = OrderedDict(( ('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')), ('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')), ('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')), + ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')), )) CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ @@ -247,6 +251,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ 'SERVICE_COMMISSION', 'SERVICE_DISCOUNT_MIN_AMOUNT', 'SERVICE_DISCOUNT', + 'SCHOOL_LOGO_IMAGE', ), 'Instagram': ( 'INSTAGRAM_CLIENT_ACCESS_TOKEN', From e7f218bd6b60d36998d4be4b4ad7a8b6154d6f7e Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Fri, 16 Mar 2018 15:48:24 +0300 Subject: [PATCH 062/120] Update yarn lock file --- web/yarn.lock | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/yarn.lock b/web/yarn.lock index 06389b2f..0a35d84c 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -887,6 +887,10 @@ backo2@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" +baguettebox.js@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/baguettebox.js/-/baguettebox.js-1.10.0.tgz#f24500b2f02433f52338cf77016ecf00cfe7f974" + balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" From ae00cb34ede15fee52bf59a5cbc51525a050b33f Mon Sep 17 00:00:00 2001 From: Sanasol Date: Mon, 19 Mar 2018 12:42:55 +0300 Subject: [PATCH 063/120] comments filter, new discount config and conditions --- project/templates/lilcity/index.html | 1 + web/src/js/modules/popup.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index ed99dd81..9a6ee2b2 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -500,6 +500,7 @@ {% block foot %}{% endblock foot %} diff --git a/web/src/js/modules/popup.js b/web/src/js/modules/popup.js index db9824b1..4f39ad08 100644 --- a/web/src/js/modules/popup.js +++ b/web/src/js/modules/popup.js @@ -73,7 +73,7 @@ $(document).ready(function () { } var text = ''; - if(weekdays.length >= 7) { + if(schoolAmountForDiscount >= price) { text = ''+price+' '+(price-schoolDiscount)+'р.'; } else { text = price+'p.'; From a17e472be570207389180045d573521c6ae25b5b Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 09:00:58 +0300 Subject: [PATCH 064/120] Disable SCHOOL_LOGO_IMAGE sretting --- api/v1/serializers/config.py | 2 +- project/settings.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index 31269d66..f6ae89dd 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -19,7 +19,7 @@ class ConfigSerializer(serializers.Serializer): INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) - SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) + # SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) def to_representation(self, instance): ret = OrderedDict() diff --git a/project/settings.py b/project/settings.py index 59b1f339..743fb0fb 100644 --- a/project/settings.py +++ b/project/settings.py @@ -243,7 +243,7 @@ CONSTANCE_CONFIG = OrderedDict(( ('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')), ('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')), ('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')), - ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')), + # ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')), )) CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ @@ -251,7 +251,7 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ 'SERVICE_COMMISSION', 'SERVICE_DISCOUNT_MIN_AMOUNT', 'SERVICE_DISCOUNT', - 'SCHOOL_LOGO_IMAGE', + # 'SCHOOL_LOGO_IMAGE', ), 'Instagram': ( 'INSTAGRAM_CLIENT_ACCESS_TOKEN', From d12ed9ae22e4e540288cf933283c48b5923d212c Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 09:27:18 +0300 Subject: [PATCH 065/120] Add is_deactivated filter for comments --- api/v1/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/v1/views.py b/api/v1/views.py index 8aaf84b8..909195d4 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -346,6 +346,16 @@ class CommentViewSet(ExtendedModelViewSet): serializer_class = CommentSerializer permission_classes = (IsAdmin,) + def get_queryset(self): + queryset = self.queryset + is_deactivated = self.request.query_params.get('is_deactivated', '0') + if is_deactivated == '0': + return queryset + elif is_deactivated == '1': + return queryset.filter(deactivated_at__isnull=True) + elif is_deactivated == '2': + return queryset.filter(deactivated_at__isnull=False) + class AuthorRequestViewSet(ExtendedModelViewSet): queryset = AuthorRequest.objects.all() From fd855045cfbd238149baecb3d26676239e3c998b Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 10:56:26 +0300 Subject: [PATCH 066/120] LIL-151. Disable cache for ajax request in CoursesView --- apps/course/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/course/views.py b/apps/course/views.py index caa3bd5f..42235e27 100644 --- a/apps/course/views.py +++ b/apps/course/views.py @@ -7,6 +7,7 @@ from django.http import JsonResponse, Http404 from django.shortcuts import get_object_or_404 from django.template import loader, Context, Template from django.views.generic import View, CreateView, DetailView, ListView, TemplateView +from django.utils.cache import add_never_cache_headers from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_http_methods @@ -245,12 +246,14 @@ class CoursesView(ListView): else: prev_url = None next_url = None - return JsonResponse({ + response = JsonResponse({ 'success': True, 'content': html, 'prev_url': prev_url, 'next_url': next_url, }) + add_never_cache_headers(response) + return response else: return super().get(request, args, kwargs) From b2c03094a32cf0756eed96020a40a690420332ce Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 11:07:34 +0300 Subject: [PATCH 067/120] LIL-226. Add sentry configs --- project/settings.py | 12 ++++++++++++ requirements.txt | 1 + sentry | 1 + 3 files changed, 14 insertions(+) create mode 160000 sentry diff --git a/project/settings.py b/project/settings.py index 743fb0fb..9785e9ca 100644 --- a/project/settings.py +++ b/project/settings.py @@ -11,10 +11,13 @@ https://docs.djangoproject.com/en/2.0/ref/settings/ """ import os +import raven + from celery.schedules import crontab from collections import OrderedDict from datetime import timedelta + # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -53,6 +56,7 @@ INSTALLED_APPS = [ 'constance', 'constance.backends.database', 'sorl.thumbnail', + 'raven.contrib.django.raven_compat', ] + [ 'apps.auth.apps', 'apps.user', @@ -290,3 +294,11 @@ if DEBUG: SWAGGER_SETTINGS = { 'DOC_EXPANSION': 'none', } + +# Raven settings +RAVEN_CONFIG = { + 'dsn': 'https://bff536c4d71c4166afb91f83b9f73d55:ca47ad791a53480b9d40a85a26abf141@sentry.io/306843', + # If you are using git, you can also automatically configure the + # release based on the git info. + 'release': raven.fetch_git_sha(BASE_DIR), +} diff --git a/requirements.txt b/requirements.txt index cf55c8a3..710cf88d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,6 +18,7 @@ gunicorn==19.7.1 mixpanel==4.3.2 psycopg2-binary==2.7.4 Pillow==5.0.0 +raven==6.6.0 requests==2.18.4 sorl-thumbnail==12.4.1 twilio==6.10.5 diff --git a/sentry b/sentry new file mode 160000 index 00000000..a514caf9 --- /dev/null +++ b/sentry @@ -0,0 +1 @@ +Subproject commit a514caf906fa22a66eaf6532a721a9393177d2ff From 8f6b5326e61cf02e67f5a22099fc892c921f0347 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 11:08:04 +0300 Subject: [PATCH 068/120] Fix sentry --- sentry | 1 - 1 file changed, 1 deletion(-) delete mode 160000 sentry diff --git a/sentry b/sentry deleted file mode 160000 index a514caf9..00000000 --- a/sentry +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a514caf906fa22a66eaf6532a721a9393177d2ff From 355a7b00fc4844ddedcebbfe6d720a5db19e9b93 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 20 Mar 2018 11:53:23 +0300 Subject: [PATCH 069/120] LIL-325. Show authors in main page --- api/v1/serializers/user.py | 1 + apps/user/admin.py | 2 +- .../migrations/0019_user_show_in_mainpage.py | 18 +++++ apps/user/models.py | 1 + project/templates/lilcity/main.html | 70 ++++--------------- project/views.py | 4 ++ 6 files changed, 38 insertions(+), 58 deletions(-) create mode 100644 apps/user/migrations/0019_user_show_in_mainpage.py diff --git a/api/v1/serializers/user.py b/api/v1/serializers/user.py index fe316cf5..f334e9a5 100644 --- a/api/v1/serializers/user.py +++ b/api/v1/serializers/user.py @@ -40,6 +40,7 @@ class UserSerializer(serializers.ModelSerializer): 'is_email_proved', 'photo', 'balance', + 'show_in_mainpage', ) read_only_fields = ( diff --git a/apps/user/admin.py b/apps/user/admin.py index 77d060dc..df3e01d9 100644 --- a/apps/user/admin.py +++ b/apps/user/admin.py @@ -15,7 +15,7 @@ class UserAdmin(BaseUserAdmin): (_('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'gender', 'about', 'photo')}), ('Facebook Auth data', {'fields': ('fb_id', 'fb_data', 'is_email_proved')}), (_('Permissions'), {'fields': ('role', 'is_active', 'is_staff', 'is_superuser', - 'groups', 'user_permissions')}), + 'groups', 'user_permissions', 'show_in_mainpage')}), (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ('Social urls', {'fields': ('instagram', 'facebook', 'twitter', 'pinterest', 'youtube', 'vkontakte', )}), ) diff --git a/apps/user/migrations/0019_user_show_in_mainpage.py b/apps/user/migrations/0019_user_show_in_mainpage.py new file mode 100644 index 00000000..e624bba1 --- /dev/null +++ b/apps/user/migrations/0019_user_show_in_mainpage.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-20 08:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0018_user_phone'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='show_in_mainpage', + field=models.BooleanField(default=False, verbose_name='Показывать на главной странице'), + ), + ] diff --git a/apps/user/models.py b/apps/user/models.py index 57ec8cd2..058df6b6 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -55,6 +55,7 @@ class User(AbstractUser): 'Верифицирован по email', default=False ) photo = models.ImageField('Фото', null=True, blank=True, upload_to='users') + show_in_mainpage = models.BooleanField('Показывать на главной странице', default=False) USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] diff --git a/project/templates/lilcity/main.html b/project/templates/lilcity/main.html index f0af1cd6..4b2623fd 100644 --- a/project/templates/lilcity/main.html +++ b/project/templates/lilcity/main.html @@ -270,74 +270,30 @@
+ {% for author in authors %}
+ {% if author.photo %} + + {% else %} + {% endif %}
-
Саша Крю, + -
@sashakru
+ {% if author.instagram %} +
{{ author.instagram }}
+ {% endif %} + {% if author.about %}
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

-
-
-
-
-
- -
-
-
Саша Крю, - #lil_персонаж -
-
@sashakru
-
-

Закончила ПХУ им К.А.Савицкого художник театра и кино. Работала с крупнейшими российскими и зарубежными - издательствами.

-

Участник и победитель международных выставок.

-

Основатель компании "Lil City".

+ {{ author.about }}
+ {% endif %}
+ {% endfor %}
Если хотите к нам в команду, то отправьте нам заявку
diff --git a/project/views.py b/project/views.py index fcedb726..035a5962 100644 --- a/project/views.py +++ b/project/views.py @@ -1,8 +1,11 @@ from django.views.generic import TemplateView +from django.contrib.auth import get_user_model from apps.course.models import Course from apps.school.models import SchoolSchedule +User = get_user_model() + class IndexView(TemplateView): template_name = 'templates/lilcity/main.html' @@ -12,6 +15,7 @@ class IndexView(TemplateView): context.update({ 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3], 'school_schedules': SchoolSchedule.objects.all(), + 'authors': User.objects.filter(role=User.AUTHOR_ROLE, show_in_mainpage=True), }) return context From 98de6fcdb0de38886fe754e5ee688689370792f2 Mon Sep 17 00:00:00 2001 From: Sanasol Date: Thu, 22 Mar 2018 11:49:06 +0300 Subject: [PATCH 070/120] comments anchor position --- apps/course/templates/course/blocks/comment.html | 3 ++- web/src/sass/_common.sass | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/course/templates/course/blocks/comment.html b/apps/course/templates/course/blocks/comment.html index 463536af..497c398d 100644 --- a/apps/course/templates/course/blocks/comment.html +++ b/apps/course/templates/course/blocks/comment.html @@ -1,6 +1,7 @@ {% load static %} {% if not node.deactivated_at %} -
+ +
{% if node.author.photo %}
diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index 66166011..e1279091 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -2488,6 +2488,11 @@ a.grey-link width: 100% .questions + &__anchor + display: block; + position: relative; + top: -110px; + visibility: hidden; &__form, &__item display: flex From 72cdbcb4f276a7e6caf6ca2f55df8ff700f3192d Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Thu, 22 Mar 2018 12:54:06 +0300 Subject: [PATCH 071/120] LIL-326. Add min school price --- project/templates/lilcity/main.html | 2 +- project/views.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/project/templates/lilcity/main.html b/project/templates/lilcity/main.html index 4b2623fd..61fef12c 100644 --- a/project/templates/lilcity/main.html +++ b/project/templates/lilcity/main.html @@ -13,7 +13,7 @@ {% endif %} class="main__btn btn" href="#" - >КУПИТЬ ДОСТУП ОТ 2000р. в мес. + >КУПИТЬ ДОСТУП ОТ {{ min_school_price }}р. в мес.
{% if messages %} diff --git a/project/views.py b/project/views.py index 035a5962..6f73fff7 100644 --- a/project/views.py +++ b/project/views.py @@ -1,5 +1,6 @@ -from django.views.generic import TemplateView +from django.db.models import Min from django.contrib.auth import get_user_model +from django.views.generic import TemplateView from apps.course.models import Course from apps.school.models import SchoolSchedule @@ -15,6 +16,7 @@ class IndexView(TemplateView): context.update({ 'course_items': Course.objects.filter(status=Course.PUBLISHED)[:3], 'school_schedules': SchoolSchedule.objects.all(), + 'min_school_price': SchoolSchedule.objects.all().aggregate(Min('month_price'))['month_price__min'], 'authors': User.objects.filter(role=User.AUTHOR_ROLE, show_in_mainpage=True), }) return context From d4aed0aead1532ba02b683f28c103b11701ea20a Mon Sep 17 00:00:00 2001 From: Sanasol Date: Thu, 22 Mar 2018 12:57:16 +0300 Subject: [PATCH 072/120] discount min price fix --- web/src/js/modules/popup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/js/modules/popup.js b/web/src/js/modules/popup.js index 4f39ad08..feaa8a8a 100644 --- a/web/src/js/modules/popup.js +++ b/web/src/js/modules/popup.js @@ -73,7 +73,7 @@ $(document).ready(function () { } var text = ''; - if(schoolAmountForDiscount >= price) { + if(schoolAmountForDiscount <= price) { text = ''+price+' '+(price-schoolDiscount)+'р.'; } else { text = price+'p.'; From 30f446331d474fa6d45adfc895be912bb9f27c14 Mon Sep 17 00:00:00 2001 From: Sanasol Date: Thu, 22 Mar 2018 13:42:42 +0300 Subject: [PATCH 073/120] phone mask --- apps/user/templates/user/profile-settings.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/user/templates/user/profile-settings.html b/apps/user/templates/user/profile-settings.html index 8eddef8c..f5f2e9fe 100644 --- a/apps/user/templates/user/profile-settings.html +++ b/apps/user/templates/user/profile-settings.html @@ -83,7 +83,7 @@
Телефон
- +
{% if form.phone.errors %}
Укажите корректно свои данные
@@ -259,4 +259,11 @@ var openFile = function(file) { reader.readAsDataURL(input.files[0]); }; + {% endblock content %} + +{% block foot %} + +{% endblock foot %} \ No newline at end of file From e1472dfc721663f2079c79b593b4df8fff7309be Mon Sep 17 00:00:00 2001 From: Vitaly Baev Date: Sun, 25 Mar 2018 11:01:55 +0300 Subject: [PATCH 074/120] =?UTF-8?q?LIL-282=20-=20=D0=9E=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BE?= =?UTF-8?q?=D0=BA=20Facebook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/js/modules/auth.js | 57 ++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/web/src/js/modules/auth.js b/web/src/js/modules/auth.js index dcda0d5e..3ec8780e 100644 --- a/web/src/js/modules/auth.js +++ b/web/src/js/modules/auth.js @@ -41,7 +41,7 @@ $(document).ready(function () { pass.hide(); login.fadeIn(); }); - + $('#password-reset__success-hide').on('click', function (e) { e.preventDefault(); $('#password-reset__form-wrapper').show(); @@ -265,36 +265,45 @@ $(document).ready(function () { }); $.ajaxSetup({cache: true}); - $.getScript('https://connect.facebook.net/en_US/sdk.js'); + load_facebook(); const facebookButton = $('button.btn_fb'); facebookButton.on('click', function () { $('.auth-register__common-error').hide(); facebookButton.addClass('loading'); - $.getScript('https://connect.facebook.net/en_US/sdk.js', function () { - FB.init({ - appId: '161924711105785', - version: 'v2.7' - }); - FB.getLoginStatus(function (response) { - if (response.status === 'connected') { - login_with_facebook(response.authResponse.accessToken); - } - else { - FB.login(function (response) { - if (response.status === 'connected') { - login_with_facebook(response.authResponse.accessToken); - } else { - facebookButton.removeClass('loading'); - $('.auth-register__common-error').text('Не удалось авторизоваться через Facebook'); - } - }, {scope: 'public_profile,email'}); - } - }); - }); + + if (facebookResponse) { + if (facebookResponse.status === 'connected') { + login_with_facebook(facebookResponse.authResponse.accessToken); + return; + } + } + + FB.login(function (response) { + if (response.status === 'connected') { + login_with_facebook(response.authResponse.accessToken); + } else { + facebookButton.removeClass('loading'); + $('.auth-register__common-error').text('Не удалось авторизоваться через Facebook'); + } + }, {scope: 'public_profile,email'}); }); }); +let facebookResponse; + +function load_facebook() { + $.getScript('https://connect.facebook.net/en_US/sdk.js', function () { + FB.init({ + appId: '161924711105785', + version: 'v2.7' + }); + FB.getLoginStatus(function (response) { + facebookResponse = response; + }); + }); +} + function login_with_facebook(accessToken) { $.ajax('/auth/facebook_login/', { method: 'POST', @@ -316,4 +325,4 @@ function login_with_facebook(accessToken) { .always(function () { $('button.btn_fb').removeClass('loading'); }); -} \ No newline at end of file +} From ad330b0dadcd9836e6d861300975df9f2394cb1c Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 08:47:10 +0300 Subject: [PATCH 075/120] Update insta access_token --- project/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings.py b/project/settings.py index 9785e9ca..7d8a067a 100644 --- a/project/settings.py +++ b/project/settings.py @@ -239,7 +239,7 @@ CONSTANCE_ADDITIONAL_FIELDS = { 'image_field': ['django.forms.ImageField', {}] } CONSTANCE_CONFIG = OrderedDict(( - ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')), + ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6', '')), ('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')), ('INSTAGRAM_PROFILE_URL', ('#', 'URL профиля Instagram.')), ('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')), From 6d1684214b0d36c44d8de96e39a2767f2ffc15b7 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 09:19:48 +0300 Subject: [PATCH 076/120] Disable silk on remote server --- project/settings.py | 13 +++++-------- project/urls.py | 4 +++- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/project/settings.py b/project/settings.py index 7d8a067a..eab7dda7 100644 --- a/project/settings.py +++ b/project/settings.py @@ -66,8 +66,6 @@ INSTALLED_APPS = [ 'apps.content', 'apps.school', ] -if DEBUG: - INSTALLED_APPS += ['silk'] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', @@ -266,12 +264,6 @@ CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ ), }) - -try: - from .local_settings import * -except ImportError: - pass - try: from paymentwall import * except ImportError: @@ -302,3 +294,8 @@ RAVEN_CONFIG = { # release based on the git info. 'release': raven.fetch_git_sha(BASE_DIR), } + +try: + from .local_settings import * +except ImportError: + pass diff --git a/project/urls.py b/project/urls.py index b0c6ddb8..b8f22484 100644 --- a/project/urls.py +++ b/project/urls.py @@ -82,6 +82,8 @@ if settings.DEBUG: from django.conf.urls.static import static from django.contrib.staticfiles.urls import staticfiles_urlpatterns - urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))] + if 'silk' in settings.INSTALLED_APPS: + urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))] + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += staticfiles_urlpatterns() From 6bf8f6affa7eeea3056bf6fbdb0f66928714bbf7 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 09:22:50 +0300 Subject: [PATCH 077/120] Fix silk miiddleware --- project/settings.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/project/settings.py b/project/settings.py index eab7dda7..ff39ee1b 100644 --- a/project/settings.py +++ b/project/settings.py @@ -78,8 +78,6 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'apps.auth.middleware.TokenAuthLoginMiddleware', ] -if DEBUG: - MIDDLEWARE += ['silk.middleware.SilkyMiddleware'] ROOT_URLCONF = 'project.urls' From 8c97b56ac262ddd2ecc9e0a30f29d91de7a95c74 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 10:48:01 +0300 Subject: [PATCH 078/120] LIL-329 Add mixpanel to all pages --- project/templates/lilcity/index.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 9a6ee2b2..440b460d 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -53,6 +53,15 @@ LIL_SERVER_TIME = "{% now 'U' %}"; LIL_SERVER_TIME_DIFF = Math.floor((new Date().getTime()) / 1000) - parseInt(LIL_SERVER_TIME); + {% block mixpanel %} + + + + {% endblock mixpanel %} From e6b514e71c8890e711e9ca190fb62ffd0680d83c Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 13:38:20 +0300 Subject: [PATCH 079/120] Remove constance & use singleton model for dynamc settings --- api/v1/serializers/config.py | 43 ++++++------------- api/v1/views.py | 19 +++----- apps/config/__init__.py | 0 apps/config/admin.py | 3 ++ apps/config/apps.py | 5 +++ apps/config/migrations/0001_initial.py | 27 ++++++++++++ .../migrations/0002_auto_20180326_1026.py | 18 ++++++++ .../migrations/0003_auto_20180326_1027.py | 23 ++++++++++ apps/config/migrations/__init__.py | 0 apps/config/models.py | 25 +++++++++++ apps/config/tests.py | 3 ++ apps/config/views.py | 3 ++ apps/content/tasks.py | 5 ++- apps/payment/models.py | 4 +- project/context_processors.py | 5 +++ project/fields.py | 14 ++++++ project/settings.py | 41 ++---------------- requirements.txt | 1 - 18 files changed, 155 insertions(+), 84 deletions(-) create mode 100644 apps/config/__init__.py create mode 100644 apps/config/admin.py create mode 100644 apps/config/apps.py create mode 100644 apps/config/migrations/0001_initial.py create mode 100644 apps/config/migrations/0002_auto_20180326_1026.py create mode 100644 apps/config/migrations/0003_auto_20180326_1027.py create mode 100644 apps/config/migrations/__init__.py create mode 100644 apps/config/models.py create mode 100644 apps/config/tests.py create mode 100644 apps/config/views.py create mode 100644 project/context_processors.py create mode 100644 project/fields.py diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index f6ae89dd..c2234a13 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -1,40 +1,25 @@ -from constance import config -from constance.admin import get_values, ConstanceForm from rest_framework import serializers -from rest_framework.fields import SkipField -from collections import OrderedDict +from apps.config.models import Config -def _set_constance_value(key, value): - form = ConstanceForm(initial=get_values()) - field = form.fields[key] - clean_value = field.clean(field.to_python(value)) - setattr(config, key, clean_value) - -class ConfigSerializer(serializers.Serializer): +class ConfigSerializer(serializers.ModelSerializer): SERVICE_COMMISSION = serializers.IntegerField(required=False) SERVICE_DISCOUNT_MIN_AMOUNT = serializers.IntegerField(required=False) SERVICE_DISCOUNT = serializers.IntegerField(required=False) INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) - # SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) - - def to_representation(self, instance): - ret = OrderedDict() - fields = self._readable_fields - for field in fields: - attribute = instance.get(field.field_name) - ret[field.field_name] = field.to_representation(attribute) - return ret - - def to_internal_value(self, data): - ret = OrderedDict(get_values()) - for k, v in data.items(): - ret[k] = v - return ret + SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) - def update(self, instance, validated_data): - for k, v in validated_data.items(): - _set_constance_value(k, v) + class Meta: + model = Config + fields = ( + 'SERVICE_COMMISSION', + 'SERVICE_DISCOUNT_MIN_AMOUNT', + 'SERVICE_DISCOUNT', + 'INSTAGRAM_CLIENT_ACCESS_TOKEN', + 'INSTAGRAM_CLIENT_SECRET', + 'INSTAGRAM_PROFILE_URL', + 'SCHOOL_LOGO_IMAGE', + ) diff --git a/api/v1/views.py b/api/v1/views.py index 909195d4..c47ce413 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,10 +1,6 @@ -from constance.admin import get_values - from django.contrib.auth import get_user_model -from rest_framework import status -from rest_framework import views, viewsets -from rest_framework import generics +from rest_framework import status, views, viewsets, generics from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response @@ -43,6 +39,7 @@ from apps.course.models import ( Material, Lesson, Like, ) +from apps.config.models import Config from apps.content.models import ( Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, @@ -327,18 +324,12 @@ class SchoolScheduleViewSet(ExtendedModelViewSet): class ConfigViewSet(generics.RetrieveUpdateAPIView): + queryset = Config.objects.all() serializer_class = ConfigSerializer permission_classes = (IsAdmin,) - def retrieve(self, request, *args, **kwargs): - serializer = ConfigSerializer(get_values()) - return Response(serializer.data) - - def patch(self, request, *args, **kwargs): - serializer = ConfigSerializer(data=request.data) - if serializer.is_valid(): - serializer.update(get_values(), serializer.validated_data) - return Response(serializer.data) + def get_object(self): + return Config.load() class CommentViewSet(ExtendedModelViewSet): diff --git a/apps/config/__init__.py b/apps/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/config/admin.py b/apps/config/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/apps/config/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/apps/config/apps.py b/apps/config/apps.py new file mode 100644 index 00000000..8d9481b1 --- /dev/null +++ b/apps/config/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ConfigConfig(AppConfig): + name = 'config' diff --git a/apps/config/migrations/0001_initial.py b/apps/config/migrations/0001_initial.py new file mode 100644 index 00000000..0aae28c7 --- /dev/null +++ b/apps/config/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Config', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('INSTAGRAM_CLIENT_ACCESS_TOKEN', models.CharField(default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6', max_length=51)), + ('INSTAGRAM_CLIENT_SECRET', models.CharField(default='2334a921425140ccb180d145dcd35b25', max_length=32)), + ('INSTAGRAM_PROFILE_URL', models.CharField(default='#', max_length=126)), + ('SERVICE_COMMISSION', models.IntegerField(default=10)), + ('SERVICE_DISCOUNT_MIN_AMOUNT', models.IntegerField(default=3500)), + ('SERVICE_DISCOUNT', models.ImageField(default=1000, upload_to='')), + ('SCHOOL_LOGO_IMAGE', models.ImageField(null=True, upload_to='')), + ], + ), + ] diff --git a/apps/config/migrations/0002_auto_20180326_1026.py b/apps/config/migrations/0002_auto_20180326_1026.py new file mode 100644 index 00000000..b91b4812 --- /dev/null +++ b/apps/config/migrations/0002_auto_20180326_1026.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.FileField(null=True, upload_to=''), + ), + ] diff --git a/apps/config/migrations/0003_auto_20180326_1027.py b/apps/config/migrations/0003_auto_20180326_1027.py new file mode 100644 index 00000000..19d5c208 --- /dev/null +++ b/apps/config/migrations/0003_auto_20180326_1027.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0002_auto_20180326_1026'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.ImageField(null=True, upload_to=''), + ), + migrations.AlterField( + model_name='config', + name='SERVICE_DISCOUNT', + field=models.IntegerField(default=1000), + ), + ] diff --git a/apps/config/migrations/__init__.py b/apps/config/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/config/models.py b/apps/config/models.py new file mode 100644 index 00000000..385717f5 --- /dev/null +++ b/apps/config/models.py @@ -0,0 +1,25 @@ +from django.db import models + + +class Config(models.Model): + INSTAGRAM_CLIENT_ACCESS_TOKEN = models.CharField( + max_length=51, default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6' + ) + INSTAGRAM_CLIENT_SECRET = models.CharField(max_length=32, default='2334a921425140ccb180d145dcd35b25') + INSTAGRAM_PROFILE_URL = models.CharField(max_length=126, default='#') + SERVICE_COMMISSION = models.IntegerField(default=10) + SERVICE_DISCOUNT_MIN_AMOUNT = models.IntegerField(default=3500) + SERVICE_DISCOUNT = models.IntegerField(default=1000) + SCHOOL_LOGO_IMAGE = models.ImageField(null=True) + + def save(self, *args, **kwargs): + self.pk = 1 + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + pass + + @classmethod + def load(cls): + obj, created = cls.objects.get_or_create(pk=1) + return obj diff --git a/apps/config/tests.py b/apps/config/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/config/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/config/views.py b/apps/config/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/apps/config/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/content/tasks.py b/apps/content/tasks.py index 9f58dcaa..ad2699ce 100644 --- a/apps/content/tasks.py +++ b/apps/content/tasks.py @@ -3,13 +3,16 @@ import json import requests import shutil -from constance import config from instagram.client import InstagramAPI from project.celery import app from time import sleep from django.conf import settings +from apps.config.models import Config + +config = Config.load() + @app.task def retrieve_photos(): diff --git a/apps/payment/models.py b/apps/payment/models.py index d3b42c8c..8f1f36d8 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -1,4 +1,3 @@ -from constance import config from paymentwall import Pingback from polymorphic.models import PolymorphicModel @@ -9,9 +8,12 @@ from django.core.validators import RegexValidator from django.utils.timezone import now from apps.course.models import Course +from apps.config.models import Config from apps.school.models import SchoolSchedule from apps.notification.utils import send_email + +config = Config.load() User = get_user_model() CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$' diff --git a/project/context_processors.py b/project/context_processors.py new file mode 100644 index 00000000..cf8dc47a --- /dev/null +++ b/project/context_processors.py @@ -0,0 +1,5 @@ +from apps.config.models import Config + + +def config(request): + return {"config": Config.load()} diff --git a/project/fields.py b/project/fields.py new file mode 100644 index 00000000..a2e4709f --- /dev/null +++ b/project/fields.py @@ -0,0 +1,14 @@ +from django.forms import ImageField as BaseImageField + + +class ImageField(BaseImageField): + + def to_internal_value(self, data): + # if data is None image field was not uploaded + if data: + file_object = super(ImageField, self).to_internal_value(data) + django_field = self._DjangoImageField() + django_field.error_messages = self.error_messages + django_field.to_python(file_object) + return file_object + return data diff --git a/project/settings.py b/project/settings.py index ff39ee1b..472f3da2 100644 --- a/project/settings.py +++ b/project/settings.py @@ -17,7 +17,6 @@ from celery.schedules import crontab from collections import OrderedDict from datetime import timedelta - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -53,8 +52,6 @@ INSTALLED_APPS = [ 'rest_framework.authtoken', 'drf_yasg', 'corsheaders', - 'constance', - 'constance.backends.database', 'sorl.thumbnail', 'raven.contrib.django.raven_compat', ] + [ @@ -64,6 +61,7 @@ INSTALLED_APPS = [ 'apps.payment', 'apps.course', 'apps.content', + 'apps.config', 'apps.school', ] @@ -90,7 +88,7 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ - 'constance.context_processors.config', + 'project.context_processors.config', 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', @@ -204,6 +202,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', @@ -228,40 +227,6 @@ CELERY_BEAT_SCHEDULE = { }, } -# Dynamic settings - -CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' -CONSTANCE_ADDITIONAL_FIELDS = { - 'image_field': ['django.forms.ImageField', {}] -} -CONSTANCE_CONFIG = OrderedDict(( - ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6', '')), - ('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')), - ('INSTAGRAM_PROFILE_URL', ('#', 'URL профиля Instagram.')), - ('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')), - ('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')), - ('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')), - ('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')), - ('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')), - # ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')), -)) - -CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ - 'Service': ( - 'SERVICE_COMMISSION', - 'SERVICE_DISCOUNT_MIN_AMOUNT', - 'SERVICE_DISCOUNT', - # 'SCHOOL_LOGO_IMAGE', - ), - 'Instagram': ( - 'INSTAGRAM_CLIENT_ACCESS_TOKEN', - 'INSTAGRAM_CLIENT_SECRET', - 'INSTAGRAM_PROFILE_URL', - 'INSTAGRAM_RESULTS_TAG', - 'INSTAGRAM_RESULTS_PATH', - ), -}) - try: from paymentwall import * except ImportError: diff --git a/requirements.txt b/requirements.txt index 710cf88d..a84ea848 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ Django==2.0.3 django-active-link==0.1.2 django-anymail[mailgun]==2.0 django-cors-headers==2.2.0 -django-constance[database]==2.1.0 django-filter==2.0.0.dev1 django-mptt==0.9.0 django-silk==2.0.0 From d439e7cd146b338af01bc348ee015273bded5d9b Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 13:55:34 +0300 Subject: [PATCH 080/120] Fix congfig migration --- apps/config/models.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/apps/config/models.py b/apps/config/models.py index 385717f5..5f19ba0f 100644 --- a/apps/config/models.py +++ b/apps/config/models.py @@ -21,5 +21,16 @@ class Config(models.Model): @classmethod def load(cls): - obj, created = cls.objects.get_or_create(pk=1) + try: + obj, created = cls.objects.get_or_create(pk=1) + except: + obj = { + 'INSTAGRAM_CLIENT_ACCESS_TOKEN': '', + 'INSTAGRAM_CLIENT_SECRET': '', + 'INSTAGRAM_PROFILE_URL': '', + 'SERVICE_COMMISSION': '', + 'SERVICE_DISCOUNT_MIN_AMOUNT': '', + 'SERVICE_DISCOUNT': '', + 'SCHOOL_LOGO_IMAGE': '', + } return obj From 68a4f4d9be8f04325c43880690a59d8ecd032e02 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 14:02:39 +0300 Subject: [PATCH 081/120] Fix INSTAGRAM_RESULTS_PATH --- apps/content/tasks.py | 2 +- project/settings.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/content/tasks.py b/apps/content/tasks.py index ad2699ce..f80a10cb 100644 --- a/apps/content/tasks.py +++ b/apps/content/tasks.py @@ -21,7 +21,7 @@ def retrieve_photos(): client_secret=config.INSTAGRAM_CLIENT_SECRET, ) recent_media, next_ = api.user_recent_media(user_id='self', count=20) - path = os.path.join(settings.BASE_DIR, config.INSTAGRAM_RESULTS_PATH) + path = os.path.join(settings.BASE_DIR, settings.INSTAGRAM_RESULTS_PATH) for idx, media in enumerate(recent_media): try: fname = os.path.join(path, f'{idx}.jpg') diff --git a/project/settings.py b/project/settings.py index 472f3da2..061ff429 100644 --- a/project/settings.py +++ b/project/settings.py @@ -258,6 +258,8 @@ RAVEN_CONFIG = { 'release': raven.fetch_git_sha(BASE_DIR), } +INSTAGRAM_RESULTS_PATH = 'media/instagram/results/' + try: from .local_settings import * except ImportError: From ece9e2789d1a26f04ce5681aaa573218cfd3a25a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 14:05:36 +0300 Subject: [PATCH 082/120] Allow null SCHOOL_LOGO_IMAGE --- api/v1/serializers/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index c2234a13..f93280b7 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -10,7 +10,7 @@ class ConfigSerializer(serializers.ModelSerializer): INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) - SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) + SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False, allow_null=True) class Meta: model = Config From 3473c5ebf77e79608773021695bc17f1c4551dac Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 15:37:30 +0300 Subject: [PATCH 083/120] LIL-324. Add MAIN_PAGE_TOP_IMAGE config --- api/v1/serializers/config.py | 2 ++ .../0004_config_main_page_top_image.py | 18 ++++++++++++++++++ apps/config/models.py | 3 +++ project/templates/lilcity/main.html | 9 ++++++++- 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 apps/config/migrations/0004_config_main_page_top_image.py diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index f93280b7..718bea00 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -11,6 +11,7 @@ class ConfigSerializer(serializers.ModelSerializer): INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False, allow_null=True) + MAIN_PAGE_TOP_IMAGE = serializers.ImageField(required=False, allow_null=True) class Meta: model = Config @@ -22,4 +23,5 @@ class ConfigSerializer(serializers.ModelSerializer): 'INSTAGRAM_CLIENT_SECRET', 'INSTAGRAM_PROFILE_URL', 'SCHOOL_LOGO_IMAGE', + 'MAIN_PAGE_TOP_IMAGE', ) diff --git a/apps/config/migrations/0004_config_main_page_top_image.py b/apps/config/migrations/0004_config_main_page_top_image.py new file mode 100644 index 00000000..5810b8c6 --- /dev/null +++ b/apps/config/migrations/0004_config_main_page_top_image.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-26 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0003_auto_20180326_1027'), + ] + + operations = [ + migrations.AddField( + model_name='config', + name='MAIN_PAGE_TOP_IMAGE', + field=models.ImageField(null=True, upload_to=''), + ), + ] diff --git a/apps/config/models.py b/apps/config/models.py index 5f19ba0f..b78f9c16 100644 --- a/apps/config/models.py +++ b/apps/config/models.py @@ -11,6 +11,7 @@ class Config(models.Model): SERVICE_DISCOUNT_MIN_AMOUNT = models.IntegerField(default=3500) SERVICE_DISCOUNT = models.IntegerField(default=1000) SCHOOL_LOGO_IMAGE = models.ImageField(null=True) + MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True) def save(self, *args, **kwargs): self.pk = 1 @@ -24,6 +25,7 @@ class Config(models.Model): try: obj, created = cls.objects.get_or_create(pk=1) except: + # This magic for migrate obj = { 'INSTAGRAM_CLIENT_ACCESS_TOKEN': '', 'INSTAGRAM_CLIENT_SECRET': '', @@ -32,5 +34,6 @@ class Config(models.Model): 'SERVICE_DISCOUNT_MIN_AMOUNT': '', 'SERVICE_DISCOUNT': '', 'SCHOOL_LOGO_IMAGE': '', + 'MAIN_PAGE_TOP_IMAGE': '', } return obj diff --git a/project/templates/lilcity/main.html b/project/templates/lilcity/main.html index 61fef12c..6e8721f0 100644 --- a/project/templates/lilcity/main.html +++ b/project/templates/lilcity/main.html @@ -2,7 +2,14 @@ {% block title %}School LIL.CITY{% endblock title %} {% block content %} -
+
Первая онлайн-школа креативного мышления для детей! 5+
Date: Mon, 26 Mar 2018 15:37:48 +0300 Subject: [PATCH 084/120] Fix retrieve_photos task --- apps/content/tasks.py | 3 +-- project/settings.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/content/tasks.py b/apps/content/tasks.py index f80a10cb..a3210c2b 100644 --- a/apps/content/tasks.py +++ b/apps/content/tasks.py @@ -11,11 +11,10 @@ from django.conf import settings from apps.config.models import Config -config = Config.load() - @app.task def retrieve_photos(): + config = Config.load() api = InstagramAPI( access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN, client_secret=config.INSTAGRAM_CLIENT_SECRET, diff --git a/project/settings.py b/project/settings.py index 061ff429..4a156847 100644 --- a/project/settings.py +++ b/project/settings.py @@ -202,7 +202,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', - 'rest_framework.renderers.BrowsableAPIRenderer', + # 'rest_framework.renderers.BrowsableAPIRenderer', ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', @@ -222,7 +222,7 @@ CELERY_TASK_SERIALIZER = 'json' CELERY_BEAT_SCHEDULE = { 'retrieve_photos_from_instagram': { 'task': 'apps.content.tasks.retrieve_photos', - 'schedule': timedelta(minutes=2) if DEBUG else crontab(minute=0, hour=0), + 'schedule': timedelta(minutes=5) if DEBUG else crontab(minute=0, hour=0), 'args': (), }, } From 1842484650bcb85e1d763f87cf815ecbd8cd4a4f Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 16:10:10 +0300 Subject: [PATCH 085/120] Add admin for config --- apps/config/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/config/admin.py b/apps/config/admin.py index 8c38f3f3..7b75773f 100644 --- a/apps/config/admin.py +++ b/apps/config/admin.py @@ -1,3 +1,5 @@ from django.contrib import admin -# Register your models here. +from .models import Config + +admin.site.register(Config) From 7b8d3e06640e9117ab1ae3967efa6dfd05116060 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 16:15:18 +0300 Subject: [PATCH 086/120] Allow blank for config images --- .../migrations/0005_auto_20180326_1314.py | 23 +++++++++++++++++++ apps/config/models.py | 4 ++-- 2 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 apps/config/migrations/0005_auto_20180326_1314.py diff --git a/apps/config/migrations/0005_auto_20180326_1314.py b/apps/config/migrations/0005_auto_20180326_1314.py new file mode 100644 index 00000000..af993523 --- /dev/null +++ b/apps/config/migrations/0005_auto_20180326_1314.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.3 on 2018-03-26 13:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0004_config_main_page_top_image'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='MAIN_PAGE_TOP_IMAGE', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + ] diff --git a/apps/config/models.py b/apps/config/models.py index b78f9c16..240fe7d5 100644 --- a/apps/config/models.py +++ b/apps/config/models.py @@ -10,8 +10,8 @@ class Config(models.Model): SERVICE_COMMISSION = models.IntegerField(default=10) SERVICE_DISCOUNT_MIN_AMOUNT = models.IntegerField(default=3500) SERVICE_DISCOUNT = models.IntegerField(default=1000) - SCHOOL_LOGO_IMAGE = models.ImageField(null=True) - MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True) + SCHOOL_LOGO_IMAGE = models.ImageField(null=True, blank=True) + MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True, blank=True) def save(self, *args, **kwargs): self.pk = 1 From 64d0952e261617171a43f065379e65bbf6e4bc78 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Mon, 26 Mar 2018 16:19:49 +0300 Subject: [PATCH 087/120] Update MAIN_PAGE_TOP_IMAGE --- project/templates/lilcity/main.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/templates/lilcity/main.html b/project/templates/lilcity/main.html index 6e8721f0..6fc3a9ea 100644 --- a/project/templates/lilcity/main.html +++ b/project/templates/lilcity/main.html @@ -5,7 +5,7 @@
Date: Tue, 27 Mar 2018 13:42:22 +0300 Subject: [PATCH 088/120] LIL-329. Add mixpanel events --- apps/course/templates/course/course.html | 4 ++- project/templates/lilcity/index.html | 3 ++- web/src/js/app.js | 1 + web/src/js/modules/mixpanel.js | 34 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 web/src/js/modules/mixpanel.js diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 4426225e..0a3c2ce4 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -14,7 +14,7 @@ {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} {% block content %} -
+
@@ -30,6 +30,7 @@ class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" {% if user.is_authenticated %} {% if not pending %} + data-course-buy href="{% url 'course-checkout' course.id %}" {% endif %} {% else %} @@ -394,6 +395,7 @@ class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" {% if user.is_authenticated %} {% if not pending %} + data-course-buy href="{% url 'course-checkout' course.id %}" {% endif %} {% else %} diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 440b460d..9a69a42d 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -47,11 +47,12 @@ viewportmeta.content = 'width=device-width, maximum-scale=1.6, initial-scale=1.0'; } } - {% block mixpanel %} diff --git a/web/src/js/app.js b/web/src/js/app.js index 81888f5c..9d0023d9 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -13,5 +13,6 @@ import "./modules/courses"; import "./modules/comments"; import "./modules/password-show"; import "./modules/profile"; +import "./modules/mixpanel"; import "../sass/app.sass"; diff --git a/web/src/js/modules/mixpanel.js b/web/src/js/modules/mixpanel.js new file mode 100644 index 00000000..d6c0626f --- /dev/null +++ b/web/src/js/modules/mixpanel.js @@ -0,0 +1,34 @@ +import $ from 'jquery'; + +$(document).ready(function (e) { + mixpanel.identify(USER_ID); + let body = $('body'), + cource = $('.course'); + + if (cource.length) { + mixpanel.track( + 'Open course', + { 'course_id': COURSE_ID } + ); + }; + + body.on('click', '[data-popup]', function (e) { + let data = $(this).data('popup'); + if (data === '.js-popup-buy') { + mixpanel.track( + 'Open school buy popup' + ); + } + }); + body.on('click', '[data-course-buy]', function (e) { + e.preventDefault(); + let href = $(this).attr('href'); + let t = mixpanel.track( + 'Click course buy button', + { 'course_id': COURSE_ID }, + function () { + window.location = href; + } + ); + }); +}); From 301c3eb38a0e051e8ab709e7726b85263f21708d Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 15:03:40 +0300 Subject: [PATCH 089/120] Add mixpanel script to project --- project/templates/lilcity/index.html | 1 + web/src/js/third_party/mixpanel-2-latest.js | 118 +++++++++ web/webpack.config.js | 12 +- web/yarn.lock | 269 +++++++++++++++++++- 4 files changed, 387 insertions(+), 13 deletions(-) create mode 100644 web/src/js/third_party/mixpanel-2-latest.js diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 9a69a42d..300fed6d 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -53,6 +53,7 @@ LIL_SERVER_TIME_DIFF = Math.floor((new Date().getTime()) / 1000) - parseInt(LIL_SERVER_TIME); USER_ID = "{{ request.user.id }}"; COURSE_ID = "{{ course.id }}"; + MIXPANEL_CUSTOM_LIB_URL = "/static/mixpanel.js"; {% block mixpanel %} diff --git a/web/src/js/third_party/mixpanel-2-latest.js b/web/src/js/third_party/mixpanel-2-latest.js new file mode 100644 index 00000000..3b1a346e --- /dev/null +++ b/web/src/js/third_party/mixpanel-2-latest.js @@ -0,0 +1,118 @@ +(function() { + var l=!0,m=null,s=!1;function C(){return function(){}} + (function(){function $(a){function b(){try{a.C=a.C||{},a.C.$__c=(a.C.$__c||0)+1,c.cookie.set("mp_"+d+"__c",a.C.$__c,1,l)}catch(b){o.error(b)}}var d=a.c("name");a.C=a.C||{};a.C.$__c=parseInt(c.cookie.get("mp_"+d+"__c"))||0;c.p(n,"submit",b);c.p(n,"change",b);var e=m;c.p(n,"mousedown",function(a){e=a.target});c.p(n,"mouseup",function(a){a.target===e&&b()})}function aa(){function a(){if(!a.Sc)V=a.Sc=l,W=s,c.a(z,function(a){a.mc()})}function b(){try{n.documentElement.doScroll("left")}catch(d){setTimeout(b,1); + return}a()}if(n.addEventListener)"complete"===n.readyState?a():n.addEventListener("DOMContentLoaded",a,s);else if(n.attachEvent){n.attachEvent("onreadystatechange",a);var d=s;try{d=t.frameElement===m}catch(e){}n.documentElement.doScroll&&d&&b()}c.p(t,"load",a,l)}function ba(){v.init=function(a,b,d){if(d)return v[d]||(v[d]=z[d]=K(a,b,d),v[d].qa()),v[d];d=v;if(z.mixpanel)d=z.mixpanel;else if(a)d=K(a,b,"mixpanel"),d.qa(),z.mixpanel=d;v=d;1===S&&(t.mixpanel=v);ca()}}function ca(){c.a(z,function(a,b){"mixpanel"!== + b&&(v[b]=a)});v._=c}function K(a,b,d){var e,f="mixpanel"===d?v:v[d];if(f&&0===S)e=f;else{if(f&&!c.isArray(f)){o.error("You have already initialized "+d);return}e=new h}e.La(a,b,d);e.people=new p;e.people.La(e);A=A||e.c("debug");e.__autotrack_enabled=e.c("autotrack");if(e.c("autotrack")){H.Tc(e.c("token"),100,100)?H.Yc()?H.W(e):(e.__autotrack_enabled=s,o.log("Disabling Automatic Event Collection because this browser is not supported")):(e.__autotrack_enabled=s,o.log("Not in active bucket: disabling Automatic Event Collection.")); + try{$(e)}catch(j){o.error(j)}}!c.d(f)&&c.isArray(f)&&(e.Ia.call(e.people,f.people),e.Ia(f));return e}function p(){}function h(){}function q(a){this.props={};this.Bb=s;this.name=a.persistence_name?"mp_"+a.persistence_name:"mp_"+a.token+"_mixpanel";var b=a.persistence;if("cookie"!==b&&"localStorage"!==b)o.U("Unknown persistence type "+b+"; falling back to cookie"),b=a.persistence="cookie";if(b="localStorage"===b){b=l;try{c.localStorage.set("__mplssupport__","xyz"),"xyz"!==c.localStorage.get("__mplssupport__")&& + (b=s),c.localStorage.remove("__mplssupport__")}catch(d){b=s}b||o.error("localStorage unsupported; falling back to cookie store")}this.v=b?c.localStorage:c.cookie;this.load();this.ac(a);this.xd(a);this.save()}function L(){this.Qb="submit"}function D(){this.Qb="click"}function y(){}function M(a){switch(typeof a.className){case "string":return a.className;case "object":return a.className.Ed||a.getAttribute("class")||"";default:return""}}function da(a){var b="";N(a)&&a.childNodes&&a.childNodes.length&& + c.a(a.childNodes,function(a){a&&3===a.nodeType&&a.textContent&&(b+=c.trim(a.textContent).split(/(\s+)/).filter(O).join("").replace(/[\r\n]/g," ").replace(/[ ]+/g," ").substring(0,255))});return c.trim(b)}function E(a,b){return a&&a.tagName&&a.tagName.toLowerCase()===b.toLowerCase()}function ea(a,b){if(!a||E(a,"html")||!(a&&1===a.nodeType))return s;switch(a.tagName.toLowerCase()){case "html":return s;case "form":return"submit"===b.type;case "input":return-1===["button","submit"].indexOf(a.getAttribute("type"))? + "change"===b.type:"click"===b.type;case "select":case "textarea":return"change"===b.type;default:return"click"===b.type}}function N(a){for(var b=a;b.parentNode&&!E(b,"body");b=b.parentNode){var d=M(b).split(" ");if(c.e(d,"mp-sensitive")||c.e(d,"mp-no-track"))return s}if(c.e(M(a).split(" "),"mp-include"))return l;if(E(a,"input")||E(a,"select")||E(a,"textarea")||"true"===a.getAttribute("contenteditable"))return s;b=a.type||"";if("string"===typeof b)switch(b.toLowerCase()){case "hidden":return s;case "password":return s}a= + a.name||a.id||"";return"string"===typeof a&&/^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i.test(a.replace(/[^a-zA-Z0-9]/g,""))?s:l}function O(a){if(a===m||c.d(a)||"string"===typeof a&&(a=c.trim(a),/^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/.test((a||"").replace(/[\- ]/g,""))||/(^\d{3}-?\d{2}-?\d{4}$)/.test(a)))return s; + return l}var A=s,t;if("undefined"===typeof window){var B={hostname:""};t={navigator:{userAgent:""},document:{location:B,referrer:""},screen:{width:0,height:0},location:B}}else t=window;var B=Array.prototype,X=Object.prototype,F=B.slice,I=X.toString,P=X.hasOwnProperty,x=t.console,G=t.navigator,n=t.document,J=t.opera,Q=t.screen,w=G.userAgent,T=Function.prototype.bind,Y=B.forEach,Z=B.indexOf,B=Array.isArray,U={},c={trim:function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},o={log:function(){if(A&& + !c.d(x)&&x)try{x.log.apply(x,arguments)}catch(a){c.a(arguments,function(a){x.log(a)})}},error:function(){if(A&&!c.d(x)&&x){var a=["Mixpanel error:"].concat(c.ia(arguments));try{x.error.apply(x,a)}catch(b){c.a(a,function(a){x.error(a)})}}},U:function(){if(!c.d(x)&&x){var a=["Mixpanel error:"].concat(c.ia(arguments));try{x.error.apply(x,a)}catch(b){c.a(a,function(a){x.error(a)})}}}};c.bind=function(a,b){var d,e;if(T&&a.bind===T)return T.apply(a,F.call(arguments,1));if(!c.$a(a))throw new TypeError;d= + F.call(arguments,2);return e=function(){if(!(this instanceof e))return a.apply(b,d.concat(F.call(arguments)));var c={};c.prototype=a.prototype;var j=new c;c.prototype=m;c=a.apply(j,d.concat(F.call(arguments)));return Object(c)===c?c:j}};c.zb=function(a){for(var b in a)"function"===typeof a[b]&&(a[b]=c.bind(a[b],a))};c.a=function(a,b,d){if(!(a===m||void 0===a))if(Y&&a.forEach===Y)a.forEach(b,d);else if(a.length===+a.length)for(var c=0,f=a.length;c/g,">").replace(/"/g,""").replace(/'/g,"'"));return a};c.extend=function(a){c.a(F.call(arguments,1),function(b){for(var d in b)void 0!==b[d]&&(a[d]=b[d])});return a};c.isArray=B||function(a){return"[object Array]"===I.call(a)};c.$a=function(a){try{return/^\s*\bfunction\b/.test(a)}catch(b){return s}};c.Xc=function(a){return!(!a||!P.call(a,"callee"))};c.ia=function(a){return!a? + []:a.ia?a.ia():c.isArray(a)||c.Xc(a)?F.call(a):c.zd(a)};c.keys=function(a){var b=[];if(a===m)return b;c.a(a,function(a,c){b[b.length]=c});return b};c.zd=function(a){var b=[];if(a===m)return b;c.a(a,function(a){b[b.length]=a});return b};c.Fd=function(a){return a};c.Ib=function(a,b){var d=s;if(a===m)return d;if(Z&&a.indexOf===Z)return-1!=a.indexOf(b);c.a(a,function(a){if(d||(d=a===b))return U});return d};c.e=function(a,b){return-1!==a.indexOf(b)};c.Kb=function(a,b){a.prototype=new b;a.rd=b.prototype}; + c.k=function(a){return a===Object(a)&&!c.isArray(a)};c.Za=function(a){if(c.k(a)){for(var b in a)if(P.call(a,b))return s;return l}return s};c.d=function(a){return void 0===a};c.ab=function(a){return"[object String]"==I.call(a)};c.Zc=function(a){return"[object Date]"==I.call(a)};c.ad=function(a){return"[object Number]"==I.call(a)};c.$c=function(a){return!!(a&&1===a.nodeType)};c.Fb=function(a){c.a(a,function(b,d){c.Zc(b)?a[d]=c.Uc(b):c.k(b)&&(a[d]=c.Fb(b))});return a};c.timestamp=function(){Date.now= + Date.now||function(){return+new Date};return Date.now()};c.Uc=function(a){function b(a){return 10>a?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.l=function(a){return function(){try{return a.apply(this,arguments)}catch(b){o.U("Implementation error. Please turn on debug and contact support@mixpanel.com."),A&&o.U(b)}}};c.gd=function(a){for(var b=["identify","_check_and_handle_notifications", + "_show_notification"],d=0;d=i;)f()}function d(){var a,b,d="",c;if('"'===i)for(;f();){if('"'===i)return f(),d;if("\\"===i)if(f(),"u"===i){for(b=c=0;4>b;b+=1){a=parseInt(f(),16);if(!isFinite(a))break;c=16*c+a}d+=String.fromCharCode(c)}else if("string"===typeof g[i])d+=g[i];else break;else d+=i}j("Bad string")} + function c(){var a;a="";"-"===i&&(a="-",f("-"));for(;"0"<=i&&"9">=i;)a+=i,f();if("."===i)for(a+=".";f()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;f();if("-"===i||"+"===i)a+=i,f();for(;"0"<=i&&"9">=i;)a+=i,f()}a=+a;if(isFinite(a))return a;j("Bad number")}function f(a){a&&a!==i&&j("Expected '"+a+"' instead of '"+i+"'");i=h.charAt(k);k+=1;return i}function j(a){a=new SyntaxError(a);a.Dd=k;a.text=h;throw a;}var k,i,g={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},h,r;r=function(){b(); + switch(i){case "{":var k;a:{var g,h={};if("{"===i){f("{");b();if("}"===i){f("}");k=h;break a}for(;i;){g=d();b();f(":");Object.hasOwnProperty.call(h,g)&&j('Duplicate key "'+g+'"');h[g]=r();b();if("}"===i){f("}");k=h;break a}f(",");b()}}j("Bad object")}return k;case "[":a:{k=[];if("["===i){f("[");b();if("]"===i){f("]");g=k;break a}for(;i;){k.push(r());b();if("]"===i){f("]");g=k;break a}f(",");b()}}j("Bad array")}return g;case '"':return d();case "-":return c();default:return"0"<=i&&"9">=i?c():a()}}; + return function(a){h=a;k=0;i=" ";a=r();b();i&&j("Syntax error");return a}}();c.xb=function(a){var b,d,e,f,j=0,k=0,i="",i=[];if(!a)return a;a=c.yd(a);do b=a.charCodeAt(j++),d=a.charCodeAt(j++),e=a.charCodeAt(j++),f=b<<16|d<<8|e,b=f>>18&63,d=f>>12&63,e=f>>6&63,f&=63,i[k++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(e)+ + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f);while(jk?c++:i=127k?String.fromCharCode(k>>6|192,k&63|128):String.fromCharCode(k>>12|224,k>>6&63|128,k&63|128);i!==m&&(c>d&&(b+=a.substring(d,c)),b+= + i,d=c=j+1)}c>d&&(b+=a.substring(d,a.length));return b};c.ib=function(){function a(){function a(b,c){var d,f=0;for(d=0;db&&delete a[d];c.Za(a)&&delete this.props.__cmpns}});q.prototype.wd=function(){if(!this.Bb)this.D(c.info.Kc()),this.Bb=l};q.prototype.bc=function(a){this.ha(c.info.kd(a))};q.prototype.hb=function(a){this.D({$initial_referrer:a||"$direct",$initial_referring_domain:c.info.Rb(a)||"$direct"},"")};q.prototype.Wc=function(){return c.xa({$initial_referrer:this.props.$initial_referrer,$initial_referring_domain:this.props.$initial_referring_domain})};q.prototype.ac= + function(a){this.Db=this.Ua=a.cookie_expiration;this.md(a.disable_persistence);this.ld(a.cross_subdomain_cookie);this.od(a.secure_cookie)};q.prototype.md=function(a){(this.disabled=a)&&this.remove()};q.prototype.ld=function(a){if(a!==this.Ra)this.Ra=a,this.remove(),this.save()};q.prototype.Vc=function(){return this.Ra};q.prototype.od=function(a){if(a!==this.Sb)this.Sb=a?l:s,this.remove(),this.save()};q.prototype.H=function(a,b){var d=this.Ka(a),e=b[a],f=this.S("$set"),j=this.S("$set_once"),g=this.S("$unset"), + i=this.S("$add"),h=this.S("$union"),n=this.S("$append",[]);"__mps"===d?(c.extend(f,e),this.I("$add",e),this.I("$union",e),this.I("$unset",e)):"__mpso"===d?(c.a(e,function(a,b){b in j||(j[b]=a)}),this.I("$unset",e)):"__mpus"===d?c.a(e,function(a){c.a([f,j,i,h],function(b){a in b&&delete b[a]});c.a(n,function(b){a in b&&delete b[a]});g[a]=l}):"__mpa"===d?(c.a(e,function(a,b){b in f?f[b]+=a:(b in i||(i[b]=0),i[b]+=a)},this),this.I("$unset",e)):"__mpu"===d?(c.a(e,function(a,b){c.isArray(a)&&(b in h|| + (h[b]=[]),h[b]=h[b].concat(a))}),this.I("$unset",e)):"__mpap"===d&&(n.push(e),this.I("$unset",e));o.log("MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):");o.log(b);this.save()};q.prototype.I=function(a,b){var d=this.Ja(a);c.d(d)||(c.a(b,function(a,b){delete d[b]},this),this.save())};q.prototype.Ka=function(a){if("$set"===a)return"__mps";if("$set_once"===a)return"__mpso";if("$unset"===a)return"__mpus";if("$add"===a)return"__mpa";if("$append"===a)return"__mpap";if("$union"===a)return"__mpu";o.error("Invalid queue:", + a)};q.prototype.Ja=function(a){return this.props[this.Ka(a)]};q.prototype.S=function(a,b){var d=this.Ka(a),b=c.d(b)?{}:b;return this.props[d]||(this.props[d]=b)};q.prototype.nd=function(a){var b=this.props.__timers||{};b[a]=(new Date).getTime();this.props.__timers=b;this.save()};q.prototype.fd=function(a){var b=(this.props.__timers||{})[a];c.d(b)||(delete this.props.__timers[a],this.save());return b};var g;h.prototype.W=function(a,b,d){if(c.d(d))o.error("You must name your new library: init(token, config, name)"); + else if("mixpanel"===d)o.error("You must initialize the main mixpanel object right after you include the Mixpanel js snippet");else return a=K(a,b,d),v[d]=a,a.qa(),a};h.prototype.La=function(a,b,d){this.__loaded=l;this.config={};this.Tb(c.extend({},ga,b,{name:d,token:a,callback_fn:("mixpanel"===d?d:"mixpanel."+d)+"._jsc"}));this._jsc=C();this.Ga=[];this.Ha=[];this.Fa=[];this.Q={disable_all_events:s,identify_called:s};this.persistence=this.cookie=new q(this.config);this.D({distinct_id:c.ib()},"")}; + h.prototype.qa=function(){this.c("loaded")(this);this.c("track_pageview")&&this.Yb()};h.prototype.mc=function(){c.a(this.Ga,function(a){this.Na.apply(this,a)},this);c.a(this.Ha,function(a){this.m.apply(this,a)},this);delete this.Ga;delete this.Ha};h.prototype.Na=function(a,b){if(this.c("img"))return o.error("You can't use DOM tracking functions with img = true."),s;if(!V)return this.Ga.push([a,b]),s;var c=(new a).W(this);return c.J.apply(c,b)};h.prototype.ra=function(a,b){if(c.d(a))return m;if(R)return function(c){a(c, + b)};var d=this._jsc,e=""+Math.floor(1E8*Math.random()),f=this.c("callback_fn")+"["+e+"]";d[e]=function(c){delete d[e];a(c,b)};return f};h.prototype.m=function(a,b,d){if(W)this.Ha.push(arguments);else{var e=this.c("verbose");b.verbose&&(e=l);this.c("test")&&(b.test=1);e&&(b.verbose=1);this.c("img")&&(b.img=1);if(!R)if(d)b.callback=d;else if(e||this.c("test"))b.callback="(function(){})";b.ip=this.c("ip")?1:0;b._=(new Date).getTime().toString();a+="?"+c.gc(b);if("img"in b){var f=n.createElement("img"); + f.src=a;n.body.appendChild(f)}else if(R)try{var j=new XMLHttpRequest;j.open("GET",a,l);j.withCredentials=l;j.onreadystatechange=function(){if(4===j.readyState)if(200===j.status){if(d)if(e){var a;try{a=c.la(j.responseText)}catch(b){o.error(b);return}d(a)}else d(Number(j.responseText))}else a="Bad HTTP status: "+j.status+" "+j.statusText,o.error(a),d&&(e?d({status:0,error:a}):d(0))};j.send(m)}catch(g){o.error(g)}else{f=n.createElement("script");f.type="text/javascript";f.async=l;f.defer=l;f.src=a;var i= + n.getElementsByTagName("script")[0];i.parentNode.insertBefore(f,i)}}};h.prototype.Ia=function(a){function b(a,b){c.a(a,function(a){this[a[0]].apply(this,a.slice(1))},b)}var d,e=[],f=[],j=[];c.a(a,function(a){a&&(d=a[0],"function"===typeof a?a.call(this):c.isArray(a)&&"alias"===d?e.push(a):c.isArray(a)&&-1!==d.indexOf("track")&&"function"===typeof this[d]?j.push(a):f.push(a))},this);b(e,this);b(f,this);b(j,this)};h.prototype.push=function(a){this.Ia([a])};h.prototype.disable=function(a){"undefined"=== + typeof a?this.Q.Oc=l:this.Fa=this.Fa.concat(a)};h.prototype.J=function(a,b,d){"function"!==typeof d&&(d=C());if(c.d(a))o.error("No event name provided to mixpanel.track");else if(this.mb(a))d(0);else{b=b||{};b.token=this.c("token");var e=this.persistence.fd(a);c.d(e)||(b.$duration=parseFloat((((new Date).getTime()-e)/1E3).toFixed(3)));this.persistence.bc(n.referrer);this.c("store_google")&&this.persistence.wd();this.c("save_referrer")&&this.persistence.hb(n.referrer);b=c.extend({},c.info.ga(),this.persistence.ga(), + b);try{if(this.c("autotrack")&&"mp_page_view"!==a&&"$create_alias"!==a)b=c.extend({},b,this.C),this.C={$__c:0},c.cookie.set("mp_"+this.c("name")+"__c",0,1,l)}catch(f){o.error(f)}e=this.c("property_blacklist");c.isArray(e)?c.a(e,function(a){delete b[a]}):o.error("Invalid value for property_blacklist config: "+e);a=c.truncate({event:a,properties:b},255);e=c.ma(a);e=c.xb(e);o.log("MIXPANEL REQUEST:");o.log(a);this.m(this.c("api_host")+"/track/",{data:e},this.ra(d,a));return a}};h.prototype.Yb=function(a){if(c.d(a))a= + n.location.href;this.J("mp_page_view",c.info.dd(a))};h.prototype.vd=function(){return this.Na.call(this,D,arguments)};h.prototype.ud=function(){return this.Na.call(this,L,arguments)};h.prototype.sd=function(a){c.d(a)?o.error("No event name provided to mixpanel.time_event"):this.mb(a)||this.persistence.nd(a)};h.prototype.ha=function(a,b){this.persistence.ha(a,b)};h.prototype.D=function(a,b,c){this.persistence.D(a,b,c)};h.prototype.Aa=function(a){this.persistence.Aa(a)};h.prototype.Ma=function(a,b){var c= + {};c[a]=b;this.ha(c)};h.prototype.Wa=function(a,b,c,e,f,j,g){a!==this.V()&&a!==this.va("__alias")&&(this.Aa("__alias"),this.Ma("distinct_id",a));this.jb(this.V());this.Q.Xa=l;this.people.qc(b,c,e,f,j,g)};h.prototype.reset=function(){this.persistence.clear();this.Q.Xa=s;this.D({distinct_id:c.ib()},"")};h.prototype.V=function(){return this.va("distinct_id")};h.prototype.Hc=function(a,b){if(a===this.va("$people_distinct_id"))return o.U("Attempting to create alias for existing People user - aborting."), + -2;var d=this;c.d(b)&&(b=this.V());if(a!==b)return this.Ma("__alias",a),this.J("$create_alias",{alias:a,distinct_id:b},function(){d.Wa(a)});o.error("alias matches current distinct_id - skipping api call.");this.Wa(a);return-1};h.prototype.bd=function(a){this.Ma("mp_name_tag",a)};h.prototype.Tb=function(a){if(c.k(a))c.extend(this.config,a),this.c("persistence_name")||(this.config.persistence_name=this.config.cookie_name),this.c("disable_persistence")||(this.config.disable_persistence=this.config.disable_cookie), + this.persistence&&this.persistence.ac(this.config),A=A||this.c("debug")};h.prototype.c=function(a){return this.config[a]};h.prototype.va=function(a){return this.persistence.props[a]};h.prototype.toString=function(){var a=this.c("name");"mixpanel"!==a&&(a="mixpanel."+a);return a};h.prototype.mb=function(a){return c.Lb(w)||this.Q.Oc||c.Ib(this.Fa,a)};h.prototype.jb=function(a){if(a&&!this.Q.Xa&&!this.c("disable_notifications")){o.log("MIXPANEL NOTIFICATION CHECK");var b=this;this.m(this.c("api_host")+ + "/decide/",{verbose:l,version:"2",lib:"web",token:this.c("token"),distinct_id:a},this.ra(function(a){a.notifications&&0");this.Mc=c.L(a.cta)||"Close";this.ea= + c.L(a.type)||"takeover";this.style=c.L(a.style)||"light";this.title=c.L(a.title)||"";this.ja=g.ic;this.Y=g.hc;this.ca=a.cta_url||m;this.Ya=a.image_url||m;this.M=a.thumb_image_url||m;this.Ba=a.video_url||m;this.ta=l;if(!this.ca)this.ca="#dismiss",this.ta=s;this.u="mini"===this.ea;if(!this.u)this.ea="takeover";this.cd=!this.u?g.Z:g.Ca;this.ub();this.wa=this.uc();this.xc()};g=h.jc;g.N=200;g.w="mixpanel-notification";g.ka=0.6;g.G=25;g.na=200;g.Z=388;g.Ca=420;g.z=85;g.Da=5;g.K=60;g.Ea=Math.round(g.K/2); + g.ic=595;g.hc=334;g.prototype.show=function(){var a=this;this.ub();this.q?(this.wc(),this.vc(),this.Bc(this.lc)):setTimeout(function(){a.show()},300)};g.prototype.Sa=c.l(function(){this.Mb||this.sb({invisible:l});var a=this.pd?this.h("video"):this.R();if(this.cc)this.Dc("bg","visible"),this.O(a,"exiting"),setTimeout(this.tb,g.N);else{var b,c,e;this.u?(b="right",c=20,e=-100):(b="top",c=g.G,e=g.na+g.G);this.oa([{s:this.h("bg"),o:"opacity",start:g.ka,j:0},{s:a,o:"opacity",start:1,j:0},{s:a,o:b,start:c, + j:e}],g.N,this.tb)}});g.prototype.O=c.l(function(a,b){b=g.w+"-"+b;"string"===typeof a&&(a=this.h(a));a.className?~(" "+a.className+" ").indexOf(" "+b+" ")||(a.className+=" "+b):a.className=b});g.prototype.Dc=c.l(function(a,b){b=g.w+"-"+b;"string"===typeof a&&(a=this.h(a));if(a.className)a.className=(" "+a.className+" ").replace(" "+b+" ","").replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")});g.prototype.oa=c.l(function(a,b,c,e){var f=this,g=s,h,i;h=1*new Date;var n,e=e||h;n=h-e;for(h=0;h=i.start?1:-1;i.F=i.start+(i.j-i.start)*n/b;if("opacity"!==i.o)i.F=Math.round(i.F);if(0=i.j||0>o&&i.F<=i.j)i.F=i.j}}if(g){for(h=0;h'):this.Hb="",this.M? + (a.push(this.M),this.Wb='
'):this.Wb="");return a};g.prototype.vc=function(){var a="",b="",c="";this.fa=n.createElement("div");this.fa.id=g.w+"-wrapper";if(this.u)a='
'+ + this.body+'
';else{var a=this.ta||this.X?"":'
',e=this.X?'
':"";this.P("ie",7)&&(e=a="");a='
"}this.ec?(b="//www.youtube.com/embed/"+this.ec+"?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080",this.fc&&(b+="&enablejsapi=1&html5=1&controls=0",c='
')): + this.dc&&(b="//player.vimeo.com/video/"+this.dc+"?autoplay=1&title=0&byline=0&portrait=0");if(this.X)this.Ad='',c='
'+c+"
";b=c+a;this.Va&&(b=(this.u?a:"")+'
'+(this.u?c:b)+"
"); + this.fa.innerHTML=('
'+b+"
").replace(/class=\"/g,'class="'+g.w+"-").replace(/id=\"/g,'id="'+g.w+"-")};g.prototype.wc=function(){this.g="dark"===this.style?{Qa:"#1d1f25",aa:"#282b32",sa:"#3a4147",yb:"#4a5157",Ic:"#32353c",Cb:"0.4",bb:"#2a3137",za:"#fff",gb:"#9498a3",Vb:"#464851",ya:"#ddd"}:{Qa:"#fff",aa:"#e7eaee",sa:"#eceff3",yb:"#f5f5f5",Ic:"#e4ecf2",Cb:"1.0",bb:"#fafafa", + za:"#5c6578",gb:"#8b949b",Vb:"#ced9e6",ya:"#7c8598"};var a="0px 0px 35px 0px rgba(45, 49, 56, 0.7)",b=a,d=a,e=g.K+2*g.Da,f=g.N/1E3+"s";this.u&&(a="none");var j={};j["@media only screen and (max-width: "+(g.Ca+20-1)+"px)"]={"#overlay":{display:"none"}};a={".flipped":{transform:"rotateY(180deg)"},"#overlay":{position:"fixed",top:"0",left:"0",width:"100%",height:"100%",overflow:"auto","text-align":"center","z-index":"10000","font-family":'"Helvetica", "Arial", sans-serif',"-webkit-font-smoothing":"antialiased", + "-moz-osx-font-smoothing":"grayscale"},"#overlay.mini":{height:"0",overflow:"visible"},"#overlay a":{width:"initial",padding:"0","text-decoration":"none","text-transform":"none",color:"inherit"},"#bgwrapper":{position:"relative",width:"100%",height:"100%"},"#bg":{position:"fixed",top:"0",left:"0",width:"100%",height:"100%","min-width":4*this.Qc+"px","min-height":4*this.Pc+"px","background-color":"black",opacity:"0.0","-ms-filter":"progid:DXImageTransform.Microsoft.Alpha(Opacity=60)",filter:"alpha(opacity=60)", + transition:"opacity "+f},"#bg.visible":{opacity:g.ka},".mini #bg":{width:"0",height:"0","min-width":"0"},"#flipcontainer":{perspective:"1000px",position:"absolute",width:"100%"},"#flipper":{position:"relative","transform-style":"preserve-3d",transition:"0.3s"},"#takeover":{position:"absolute",left:"50%",width:g.Z+"px","margin-left":Math.round(-g.Z/2)+"px","backface-visibility":"hidden",transform:"rotateY(0deg)",opacity:"0.0",top:g.na+"px",transition:"opacity "+f+", top "+f},"#takeover.visible":{opacity:"1.0", + top:g.G+"px"},"#takeover.exiting":{opacity:"0.0",top:g.na+"px"},"#thumbspacer":{height:g.Ea+"px"},"#thumbborder-wrapper":{position:"absolute",top:-g.Da+"px",left:g.Z/2-g.Ea-g.Da+"px",width:e+"px",height:e/2+"px",overflow:"hidden"},"#thumbborder":{position:"absolute",width:e+"px",height:e+"px","border-radius":e+"px","background-color":this.g.aa,opacity:"0.5"},"#thumbnail":{position:"absolute",top:"0px",left:g.Z/2-g.Ea+"px",width:g.K+"px",height:g.K+"px",overflow:"hidden","z-index":"100","border-radius":g.K+ + "px"},"#mini":{position:"absolute",right:"20px",top:g.G+"px",width:this.cd+"px",height:2*g.z+"px","margin-top":20-g.z+"px","backface-visibility":"hidden",opacity:"0.0",transform:"rotateX(90deg)",transition:"opacity 0.3s, transform 0.3s, right 0.3s"},"#mini.visible":{opacity:"1.0",transform:"rotateX(0deg)"},"#mini.exiting":{opacity:"0.0",right:"-150px"},"#mainbox":{"border-radius":"4px","box-shadow":a,"text-align":"center","background-color":this.g.Qa,"font-size":"14px",color:this.g.gb},"#mini #mainbox":{height:g.z+ + "px","margin-top":g.z+"px","border-radius":"3px",transition:"background-color "+f},"#mini-border":{height:g.z+6+"px",width:g.Ca+6+"px",position:"absolute",top:"-3px",left:"-3px","margin-top":g.z+"px","border-radius":"6px",opacity:"0.25","background-color":"#fff","z-index":"-1","box-shadow":d},"#mini-icon":{position:"relative",display:"inline-block",width:"75px",height:g.z+"px","border-radius":"3px 0 0 3px","background-color":this.g.aa,background:"linear-gradient(135deg, "+this.g.yb+" 0%, "+this.g.aa+ + " 100%)",transition:"background-color "+f},"#mini:hover #mini-icon":{"background-color":this.g.bb},"#mini:hover #mainbox":{"background-color":this.g.bb},"#mini-icon-img":{position:"absolute","background-image":"url("+this.M+")",width:"48px",height:"48px",top:"20px",left:"12px"},"#content":{padding:"30px 20px 0px 20px"},"#mini-content":{"text-align":"left",height:g.z+"px",cursor:"pointer"},"#img":{width:"328px","margin-top":"30px","border-radius":"5px"},"#title":{"max-height":"600px",overflow:"hidden", + "word-wrap":"break-word",padding:"25px 0px 20px 0px","font-size":"19px","font-weight":"bold",color:this.g.za},"#body":{"max-height":"600px","margin-bottom":"25px",overflow:"hidden","word-wrap":"break-word","line-height":"21px","font-size":"15px","font-weight":"normal","text-align":"left"},"#mini #body":{display:"inline-block","max-width":"250px",margin:"0 0 0 30px",height:g.z+"px","font-size":"16px","letter-spacing":"0.8px",color:this.g.za},"#mini #body-text":{display:"table",height:g.z+"px"},"#mini #body-text div":{display:"table-cell", + "vertical-align":"middle"},"#tagline":{"margin-bottom":"15px","font-size":"10px","font-weight":"600","letter-spacing":"0.8px",color:"#ccd7e0","text-align":"left"},"#tagline a":{color:this.g.Vb,transition:"color "+f},"#tagline a:hover":{color:this.g.ya},"#cancel":{position:"absolute",right:"0",width:"8px",height:"8px",padding:"10px","border-radius":"20px",margin:"12px 12px 0 0","box-sizing":"content-box",cursor:"pointer",transition:"background-color "+f},"#mini #cancel":{margin:"7px 7px 0 0"},"#cancel-icon":{width:"8px", + height:"8px",overflow:"hidden","background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)",opacity:this.g.Cb},"#cancel:hover":{"background-color":this.g.sa},"#button":{display:"block",height:"60px","line-height":"60px","text-align":"center","background-color":this.g.aa,"border-radius":"0 0 4px 4px",overflow:"hidden",cursor:"pointer",transition:"background-color "+f},"#button-close":{display:"inline-block",width:"9px",height:"60px","margin-right":"8px","vertical-align":"top", + "background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-"+this.style+".png)","background-repeat":"no-repeat","background-position":"0px 25px"},"#button-play":{display:"inline-block",width:"30px",height:"60px","margin-left":"15px","background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-"+this.style+"-small.png)","background-repeat":"no-repeat","background-position":"0px 15px"},"a#button-link":{display:"inline-block","vertical-align":"top","text-align":"center", + "font-size":"17px","font-weight":"bold",overflow:"hidden","word-wrap":"break-word",color:this.g.za,transition:"color "+f},"#button:hover":{"background-color":this.g.sa,color:this.g.ya},"#button:hover a":{color:this.g.ya},"#video-noflip":{position:"relative",top:2*-this.Y+"px"},"#video-flip":{"backface-visibility":"hidden",transform:"rotateY(180deg)"},"#video":{position:"absolute",width:this.ja-1+"px",height:this.Y+"px",top:g.G+"px","margin-top":"100px",left:"50%","margin-left":Math.round(-this.ja/ + 2)+"px",overflow:"hidden","border-radius":"5px","box-shadow":b,transform:"translateZ(1px)",transition:"opacity "+f+", top "+f},"#video.exiting":{opacity:"0.0",top:this.Y+"px"},"#video-holder":{position:"absolute",width:this.ja-1+"px",height:this.Y+"px",overflow:"hidden","border-radius":"5px"},"#video-frame":{"margin-left":"-1px",width:this.ja+"px"},"#video-controls":{opacity:"0",transition:"opacity 0.5s"},"#video:hover #video-controls":{opacity:"1.0"},"#video .video-progress-el":{position:"absolute", + bottom:"0",height:"25px","border-radius":"0 0 0 5px"},"#video-progress":{width:"90%"},"#video-progress-total":{width:"100%","background-color":this.g.Qa,opacity:"0.7"},"#video-elapsed":{width:"0","background-color":"#6cb6f5",opacity:"0.9"},"#video #video-time":{width:"10%",right:"0","font-size":"11px","line-height":"25px",color:this.g.gb,"background-color":"#666","border-radius":"0 0 5px 0"}};this.P("ie",8)&&c.extend(a,{"* html #overlay":{position:"absolute"},"* html #bg":{position:"absolute"},"html, body":{height:"100%"}}); + this.P("ie",7)&&c.extend(a,{"#mini #body":{display:"inline",zoom:"1",border:"1px solid "+this.g.sa},"#mini #body-text":{padding:"20px"},"#mini #mini-icon":{display:"none"}});var b="backface-visibility,border-radius,box-shadow,opacity,perspective,transform,transform-style,transition".split(","),d=["khtml","moz","ms","o","webkit"],h;for(h in a)for(e=0;e(v.__SV||0)?o.U("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."):(c.a(v._i,function(a){a&&c.isArray(a)&&(z[a[a.length-1]]=K.apply(this,a))}),ba(),v.init(),c.a(z,function(a){a.qa()}),aa())})()})(); + })(); diff --git a/web/webpack.config.js b/web/webpack.config.js index bb23a500..663183bd 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -6,7 +6,8 @@ const ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: { app: "./src/js/app.js", - courseRedactor: "./src/js/course-redactor.js" + courseRedactor: "./src/js/course-redactor.js", + mixpanel: "./src/js/third_party/mixpanel-2-latest.js" }, output: { path: path.join(__dirname, "build"), @@ -20,7 +21,7 @@ module.exports = { loaders: [ { test: /\.js$/, - exclude: /(node_modules|bower_components)/, + exclude: /(node_modules|bower_components|third_party)/, use: { loader: 'babel-loader', options: { @@ -28,6 +29,11 @@ module.exports = { } } }, + { + test: /third_party\/.*\.js$/, + exclude: /(node_modules|bower_components)/, + loader: 'url-loader' + }, { test: /\.css$/, use: ExtractTextPlugin.extract({ @@ -97,4 +103,4 @@ if (NODE_ENV === 'production') { } }) ); -} \ No newline at end of file +} diff --git a/web/yarn.lock b/web/yarn.lock index 0a35d84c..b8b74a0f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -168,7 +168,7 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" -aproba@^1.0.3: +aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -260,7 +260,7 @@ arraybuffer.slice@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" -arrify@^1.0.0: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -961,7 +961,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.0.5, bluebird@^3.1.1: +bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1139,6 +1139,10 @@ bs-recipes@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.3.4.tgz#0d2d4d48a718c8c044769fdc4f89592dc8b69585" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1165,6 +1169,24 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1323,6 +1345,10 @@ chokidar@1.7.0, chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -1503,6 +1529,15 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + config-chain@~1.1.5: version "1.1.11" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" @@ -1562,10 +1597,34 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-webpack-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c" + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + p-limit "^1.0.0" + serialize-javascript "^1.4.0" + core-js@^2.4.0, core-js@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" @@ -1770,6 +1829,10 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -1946,6 +2009,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + doctypes@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" @@ -1999,6 +2069,15 @@ duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + duplexify@^3.5.0: version "3.5.3" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" @@ -2068,7 +2147,7 @@ encodeurl@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" -end-of-stream@^1.0.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" dependencies: @@ -2495,6 +2574,13 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flush-write-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + follow-redirects@^1.2.5: version "1.4.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" @@ -2559,6 +2645,13 @@ fresh@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -2577,6 +2670,15 @@ fs-extra@3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2711,7 +2813,7 @@ glob@^5.0.12: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2763,6 +2865,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + globule@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" @@ -2785,7 +2898,7 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -graceful-fs@4.X, graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@4.X, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3294,6 +3407,14 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore@^3.3.5: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + ilyabirman-likely@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/ilyabirman-likely/-/ilyabirman-likely-2.3.0.tgz#4462becc5dedeb36b74bf4ba339a0ceab820785f" @@ -3302,6 +3423,10 @@ immutable@3.8.2, immutable@^3.7.6: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + in-publish@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" @@ -3474,7 +3599,7 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3506,6 +3631,12 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-number-like@^1.0.3: version "1.0.8" resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3" @@ -4411,6 +4542,21 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + mixin-deep@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a" @@ -4435,6 +4581,17 @@ moment@^2.20.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -4744,7 +4901,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.3, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -4822,7 +4979,7 @@ p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" -p-limit@^1.1.0: +p-limit@^1.0.0, p-limit@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" dependencies: @@ -4842,6 +4999,14 @@ pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + parse-asn1@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" @@ -4953,6 +5118,12 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5324,10 +5495,18 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + promise@^7.0.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -5449,6 +5628,21 @@ pug-walk@^1.1.5: pug-runtime "^2.0.3" pug-strip-comments "^1.0.2" +pump@^2.0.0, pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -5551,6 +5745,18 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" +"readable-stream@1 || 2", readable-stream@^2.0.4, readable-stream@^2.2.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -5811,7 +6017,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -5824,6 +6030,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + run-sequence@^1.1.5: version "1.2.2" resolved "https://registry.yarnpkg.com/run-sequence/-/run-sequence-1.2.2.tgz#5095a0bebe98733b0140bd08dd80ec030ddacdeb" @@ -5916,6 +6128,10 @@ sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + serve-index@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b" @@ -6226,6 +6442,12 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + dependencies: + safe-buffer "^5.1.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -6258,6 +6480,13 @@ stream-consume@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + stream-http@^2.7.2: version "2.8.0" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" @@ -6563,6 +6792,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + ua-parser-js@0.7.12: version "0.7.12" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb" @@ -6623,6 +6856,18 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" @@ -6988,6 +7233,10 @@ y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" From aedb166a2314d85d14b4b9acf5bd8702af9f01d0 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 15:44:18 +0300 Subject: [PATCH 090/120] Fix mixpanel script --- project/templates/lilcity/index.html | 2 +- web/src/js/third_party/mixpanel-2-latest.js | 5795 ++++++++++++++++++- web/webpack.config.js | 7 +- 3 files changed, 5685 insertions(+), 119 deletions(-) diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 300fed6d..dbdbcfe2 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -53,7 +53,7 @@ LIL_SERVER_TIME_DIFF = Math.floor((new Date().getTime()) / 1000) - parseInt(LIL_SERVER_TIME); USER_ID = "{{ request.user.id }}"; COURSE_ID = "{{ course.id }}"; - MIXPANEL_CUSTOM_LIB_URL = "/static/mixpanel.js"; + MIXPANEL_CUSTOM_LIB_URL = "/static/mixpanel-2-latest.js"; {% block mixpanel %} diff --git a/web/src/js/third_party/mixpanel-2-latest.js b/web/src/js/third_party/mixpanel-2-latest.js index 3b1a346e..ddf58895 100644 --- a/web/src/js/third_party/mixpanel-2-latest.js +++ b/web/src/js/third_party/mixpanel-2-latest.js @@ -1,118 +1,5679 @@ -(function() { - var l=!0,m=null,s=!1;function C(){return function(){}} - (function(){function $(a){function b(){try{a.C=a.C||{},a.C.$__c=(a.C.$__c||0)+1,c.cookie.set("mp_"+d+"__c",a.C.$__c,1,l)}catch(b){o.error(b)}}var d=a.c("name");a.C=a.C||{};a.C.$__c=parseInt(c.cookie.get("mp_"+d+"__c"))||0;c.p(n,"submit",b);c.p(n,"change",b);var e=m;c.p(n,"mousedown",function(a){e=a.target});c.p(n,"mouseup",function(a){a.target===e&&b()})}function aa(){function a(){if(!a.Sc)V=a.Sc=l,W=s,c.a(z,function(a){a.mc()})}function b(){try{n.documentElement.doScroll("left")}catch(d){setTimeout(b,1); - return}a()}if(n.addEventListener)"complete"===n.readyState?a():n.addEventListener("DOMContentLoaded",a,s);else if(n.attachEvent){n.attachEvent("onreadystatechange",a);var d=s;try{d=t.frameElement===m}catch(e){}n.documentElement.doScroll&&d&&b()}c.p(t,"load",a,l)}function ba(){v.init=function(a,b,d){if(d)return v[d]||(v[d]=z[d]=K(a,b,d),v[d].qa()),v[d];d=v;if(z.mixpanel)d=z.mixpanel;else if(a)d=K(a,b,"mixpanel"),d.qa(),z.mixpanel=d;v=d;1===S&&(t.mixpanel=v);ca()}}function ca(){c.a(z,function(a,b){"mixpanel"!== - b&&(v[b]=a)});v._=c}function K(a,b,d){var e,f="mixpanel"===d?v:v[d];if(f&&0===S)e=f;else{if(f&&!c.isArray(f)){o.error("You have already initialized "+d);return}e=new h}e.La(a,b,d);e.people=new p;e.people.La(e);A=A||e.c("debug");e.__autotrack_enabled=e.c("autotrack");if(e.c("autotrack")){H.Tc(e.c("token"),100,100)?H.Yc()?H.W(e):(e.__autotrack_enabled=s,o.log("Disabling Automatic Event Collection because this browser is not supported")):(e.__autotrack_enabled=s,o.log("Not in active bucket: disabling Automatic Event Collection.")); - try{$(e)}catch(j){o.error(j)}}!c.d(f)&&c.isArray(f)&&(e.Ia.call(e.people,f.people),e.Ia(f));return e}function p(){}function h(){}function q(a){this.props={};this.Bb=s;this.name=a.persistence_name?"mp_"+a.persistence_name:"mp_"+a.token+"_mixpanel";var b=a.persistence;if("cookie"!==b&&"localStorage"!==b)o.U("Unknown persistence type "+b+"; falling back to cookie"),b=a.persistence="cookie";if(b="localStorage"===b){b=l;try{c.localStorage.set("__mplssupport__","xyz"),"xyz"!==c.localStorage.get("__mplssupport__")&& - (b=s),c.localStorage.remove("__mplssupport__")}catch(d){b=s}b||o.error("localStorage unsupported; falling back to cookie store")}this.v=b?c.localStorage:c.cookie;this.load();this.ac(a);this.xd(a);this.save()}function L(){this.Qb="submit"}function D(){this.Qb="click"}function y(){}function M(a){switch(typeof a.className){case "string":return a.className;case "object":return a.className.Ed||a.getAttribute("class")||"";default:return""}}function da(a){var b="";N(a)&&a.childNodes&&a.childNodes.length&& - c.a(a.childNodes,function(a){a&&3===a.nodeType&&a.textContent&&(b+=c.trim(a.textContent).split(/(\s+)/).filter(O).join("").replace(/[\r\n]/g," ").replace(/[ ]+/g," ").substring(0,255))});return c.trim(b)}function E(a,b){return a&&a.tagName&&a.tagName.toLowerCase()===b.toLowerCase()}function ea(a,b){if(!a||E(a,"html")||!(a&&1===a.nodeType))return s;switch(a.tagName.toLowerCase()){case "html":return s;case "form":return"submit"===b.type;case "input":return-1===["button","submit"].indexOf(a.getAttribute("type"))? - "change"===b.type:"click"===b.type;case "select":case "textarea":return"change"===b.type;default:return"click"===b.type}}function N(a){for(var b=a;b.parentNode&&!E(b,"body");b=b.parentNode){var d=M(b).split(" ");if(c.e(d,"mp-sensitive")||c.e(d,"mp-no-track"))return s}if(c.e(M(a).split(" "),"mp-include"))return l;if(E(a,"input")||E(a,"select")||E(a,"textarea")||"true"===a.getAttribute("contenteditable"))return s;b=a.type||"";if("string"===typeof b)switch(b.toLowerCase()){case "hidden":return s;case "password":return s}a= - a.name||a.id||"";return"string"===typeof a&&/^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i.test(a.replace(/[^a-zA-Z0-9]/g,""))?s:l}function O(a){if(a===m||c.d(a)||"string"===typeof a&&(a=c.trim(a),/^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/.test((a||"").replace(/[\- ]/g,""))||/(^\d{3}-?\d{2}-?\d{4}$)/.test(a)))return s; - return l}var A=s,t;if("undefined"===typeof window){var B={hostname:""};t={navigator:{userAgent:""},document:{location:B,referrer:""},screen:{width:0,height:0},location:B}}else t=window;var B=Array.prototype,X=Object.prototype,F=B.slice,I=X.toString,P=X.hasOwnProperty,x=t.console,G=t.navigator,n=t.document,J=t.opera,Q=t.screen,w=G.userAgent,T=Function.prototype.bind,Y=B.forEach,Z=B.indexOf,B=Array.isArray,U={},c={trim:function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")}},o={log:function(){if(A&& - !c.d(x)&&x)try{x.log.apply(x,arguments)}catch(a){c.a(arguments,function(a){x.log(a)})}},error:function(){if(A&&!c.d(x)&&x){var a=["Mixpanel error:"].concat(c.ia(arguments));try{x.error.apply(x,a)}catch(b){c.a(a,function(a){x.error(a)})}}},U:function(){if(!c.d(x)&&x){var a=["Mixpanel error:"].concat(c.ia(arguments));try{x.error.apply(x,a)}catch(b){c.a(a,function(a){x.error(a)})}}}};c.bind=function(a,b){var d,e;if(T&&a.bind===T)return T.apply(a,F.call(arguments,1));if(!c.$a(a))throw new TypeError;d= - F.call(arguments,2);return e=function(){if(!(this instanceof e))return a.apply(b,d.concat(F.call(arguments)));var c={};c.prototype=a.prototype;var j=new c;c.prototype=m;c=a.apply(j,d.concat(F.call(arguments)));return Object(c)===c?c:j}};c.zb=function(a){for(var b in a)"function"===typeof a[b]&&(a[b]=c.bind(a[b],a))};c.a=function(a,b,d){if(!(a===m||void 0===a))if(Y&&a.forEach===Y)a.forEach(b,d);else if(a.length===+a.length)for(var c=0,f=a.length;c/g,">").replace(/"/g,""").replace(/'/g,"'"));return a};c.extend=function(a){c.a(F.call(arguments,1),function(b){for(var d in b)void 0!==b[d]&&(a[d]=b[d])});return a};c.isArray=B||function(a){return"[object Array]"===I.call(a)};c.$a=function(a){try{return/^\s*\bfunction\b/.test(a)}catch(b){return s}};c.Xc=function(a){return!(!a||!P.call(a,"callee"))};c.ia=function(a){return!a? - []:a.ia?a.ia():c.isArray(a)||c.Xc(a)?F.call(a):c.zd(a)};c.keys=function(a){var b=[];if(a===m)return b;c.a(a,function(a,c){b[b.length]=c});return b};c.zd=function(a){var b=[];if(a===m)return b;c.a(a,function(a){b[b.length]=a});return b};c.Fd=function(a){return a};c.Ib=function(a,b){var d=s;if(a===m)return d;if(Z&&a.indexOf===Z)return-1!=a.indexOf(b);c.a(a,function(a){if(d||(d=a===b))return U});return d};c.e=function(a,b){return-1!==a.indexOf(b)};c.Kb=function(a,b){a.prototype=new b;a.rd=b.prototype}; - c.k=function(a){return a===Object(a)&&!c.isArray(a)};c.Za=function(a){if(c.k(a)){for(var b in a)if(P.call(a,b))return s;return l}return s};c.d=function(a){return void 0===a};c.ab=function(a){return"[object String]"==I.call(a)};c.Zc=function(a){return"[object Date]"==I.call(a)};c.ad=function(a){return"[object Number]"==I.call(a)};c.$c=function(a){return!!(a&&1===a.nodeType)};c.Fb=function(a){c.a(a,function(b,d){c.Zc(b)?a[d]=c.Uc(b):c.k(b)&&(a[d]=c.Fb(b))});return a};c.timestamp=function(){Date.now= - Date.now||function(){return+new Date};return Date.now()};c.Uc=function(a){function b(a){return 10>a?"0"+a:a}return a.getUTCFullYear()+"-"+b(a.getUTCMonth()+1)+"-"+b(a.getUTCDate())+"T"+b(a.getUTCHours())+":"+b(a.getUTCMinutes())+":"+b(a.getUTCSeconds())};c.l=function(a){return function(){try{return a.apply(this,arguments)}catch(b){o.U("Implementation error. Please turn on debug and contact support@mixpanel.com."),A&&o.U(b)}}};c.gd=function(a){for(var b=["identify","_check_and_handle_notifications", - "_show_notification"],d=0;d=i;)f()}function d(){var a,b,d="",c;if('"'===i)for(;f();){if('"'===i)return f(),d;if("\\"===i)if(f(),"u"===i){for(b=c=0;4>b;b+=1){a=parseInt(f(),16);if(!isFinite(a))break;c=16*c+a}d+=String.fromCharCode(c)}else if("string"===typeof g[i])d+=g[i];else break;else d+=i}j("Bad string")} - function c(){var a;a="";"-"===i&&(a="-",f("-"));for(;"0"<=i&&"9">=i;)a+=i,f();if("."===i)for(a+=".";f()&&"0"<=i&&"9">=i;)a+=i;if("e"===i||"E"===i){a+=i;f();if("-"===i||"+"===i)a+=i,f();for(;"0"<=i&&"9">=i;)a+=i,f()}a=+a;if(isFinite(a))return a;j("Bad number")}function f(a){a&&a!==i&&j("Expected '"+a+"' instead of '"+i+"'");i=h.charAt(k);k+=1;return i}function j(a){a=new SyntaxError(a);a.Dd=k;a.text=h;throw a;}var k,i,g={'"':'"',"\\":"\\","/":"/",b:"\u0008",f:"\u000c",n:"\n",r:"\r",t:"\t"},h,r;r=function(){b(); - switch(i){case "{":var k;a:{var g,h={};if("{"===i){f("{");b();if("}"===i){f("}");k=h;break a}for(;i;){g=d();b();f(":");Object.hasOwnProperty.call(h,g)&&j('Duplicate key "'+g+'"');h[g]=r();b();if("}"===i){f("}");k=h;break a}f(",");b()}}j("Bad object")}return k;case "[":a:{k=[];if("["===i){f("[");b();if("]"===i){f("]");g=k;break a}for(;i;){k.push(r());b();if("]"===i){f("]");g=k;break a}f(",");b()}}j("Bad array")}return g;case '"':return d();case "-":return c();default:return"0"<=i&&"9">=i?c():a()}}; - return function(a){h=a;k=0;i=" ";a=r();b();i&&j("Syntax error");return a}}();c.xb=function(a){var b,d,e,f,j=0,k=0,i="",i=[];if(!a)return a;a=c.yd(a);do b=a.charCodeAt(j++),d=a.charCodeAt(j++),e=a.charCodeAt(j++),f=b<<16|d<<8|e,b=f>>18&63,d=f>>12&63,e=f>>6&63,f&=63,i[k++]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(b)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(d)+"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(e)+ - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".charAt(f);while(jk?c++:i=127k?String.fromCharCode(k>>6|192,k&63|128):String.fromCharCode(k>>12|224,k>>6&63|128,k&63|128);i!==m&&(c>d&&(b+=a.substring(d,c)),b+= - i,d=c=j+1)}c>d&&(b+=a.substring(d,a.length));return b};c.ib=function(){function a(){function a(b,c){var d,f=0;for(d=0;db&&delete a[d];c.Za(a)&&delete this.props.__cmpns}});q.prototype.wd=function(){if(!this.Bb)this.D(c.info.Kc()),this.Bb=l};q.prototype.bc=function(a){this.ha(c.info.kd(a))};q.prototype.hb=function(a){this.D({$initial_referrer:a||"$direct",$initial_referring_domain:c.info.Rb(a)||"$direct"},"")};q.prototype.Wc=function(){return c.xa({$initial_referrer:this.props.$initial_referrer,$initial_referring_domain:this.props.$initial_referring_domain})};q.prototype.ac= - function(a){this.Db=this.Ua=a.cookie_expiration;this.md(a.disable_persistence);this.ld(a.cross_subdomain_cookie);this.od(a.secure_cookie)};q.prototype.md=function(a){(this.disabled=a)&&this.remove()};q.prototype.ld=function(a){if(a!==this.Ra)this.Ra=a,this.remove(),this.save()};q.prototype.Vc=function(){return this.Ra};q.prototype.od=function(a){if(a!==this.Sb)this.Sb=a?l:s,this.remove(),this.save()};q.prototype.H=function(a,b){var d=this.Ka(a),e=b[a],f=this.S("$set"),j=this.S("$set_once"),g=this.S("$unset"), - i=this.S("$add"),h=this.S("$union"),n=this.S("$append",[]);"__mps"===d?(c.extend(f,e),this.I("$add",e),this.I("$union",e),this.I("$unset",e)):"__mpso"===d?(c.a(e,function(a,b){b in j||(j[b]=a)}),this.I("$unset",e)):"__mpus"===d?c.a(e,function(a){c.a([f,j,i,h],function(b){a in b&&delete b[a]});c.a(n,function(b){a in b&&delete b[a]});g[a]=l}):"__mpa"===d?(c.a(e,function(a,b){b in f?f[b]+=a:(b in i||(i[b]=0),i[b]+=a)},this),this.I("$unset",e)):"__mpu"===d?(c.a(e,function(a,b){c.isArray(a)&&(b in h|| - (h[b]=[]),h[b]=h[b].concat(a))}),this.I("$unset",e)):"__mpap"===d&&(n.push(e),this.I("$unset",e));o.log("MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):");o.log(b);this.save()};q.prototype.I=function(a,b){var d=this.Ja(a);c.d(d)||(c.a(b,function(a,b){delete d[b]},this),this.save())};q.prototype.Ka=function(a){if("$set"===a)return"__mps";if("$set_once"===a)return"__mpso";if("$unset"===a)return"__mpus";if("$add"===a)return"__mpa";if("$append"===a)return"__mpap";if("$union"===a)return"__mpu";o.error("Invalid queue:", - a)};q.prototype.Ja=function(a){return this.props[this.Ka(a)]};q.prototype.S=function(a,b){var d=this.Ka(a),b=c.d(b)?{}:b;return this.props[d]||(this.props[d]=b)};q.prototype.nd=function(a){var b=this.props.__timers||{};b[a]=(new Date).getTime();this.props.__timers=b;this.save()};q.prototype.fd=function(a){var b=(this.props.__timers||{})[a];c.d(b)||(delete this.props.__timers[a],this.save());return b};var g;h.prototype.W=function(a,b,d){if(c.d(d))o.error("You must name your new library: init(token, config, name)"); - else if("mixpanel"===d)o.error("You must initialize the main mixpanel object right after you include the Mixpanel js snippet");else return a=K(a,b,d),v[d]=a,a.qa(),a};h.prototype.La=function(a,b,d){this.__loaded=l;this.config={};this.Tb(c.extend({},ga,b,{name:d,token:a,callback_fn:("mixpanel"===d?d:"mixpanel."+d)+"._jsc"}));this._jsc=C();this.Ga=[];this.Ha=[];this.Fa=[];this.Q={disable_all_events:s,identify_called:s};this.persistence=this.cookie=new q(this.config);this.D({distinct_id:c.ib()},"")}; - h.prototype.qa=function(){this.c("loaded")(this);this.c("track_pageview")&&this.Yb()};h.prototype.mc=function(){c.a(this.Ga,function(a){this.Na.apply(this,a)},this);c.a(this.Ha,function(a){this.m.apply(this,a)},this);delete this.Ga;delete this.Ha};h.prototype.Na=function(a,b){if(this.c("img"))return o.error("You can't use DOM tracking functions with img = true."),s;if(!V)return this.Ga.push([a,b]),s;var c=(new a).W(this);return c.J.apply(c,b)};h.prototype.ra=function(a,b){if(c.d(a))return m;if(R)return function(c){a(c, - b)};var d=this._jsc,e=""+Math.floor(1E8*Math.random()),f=this.c("callback_fn")+"["+e+"]";d[e]=function(c){delete d[e];a(c,b)};return f};h.prototype.m=function(a,b,d){if(W)this.Ha.push(arguments);else{var e=this.c("verbose");b.verbose&&(e=l);this.c("test")&&(b.test=1);e&&(b.verbose=1);this.c("img")&&(b.img=1);if(!R)if(d)b.callback=d;else if(e||this.c("test"))b.callback="(function(){})";b.ip=this.c("ip")?1:0;b._=(new Date).getTime().toString();a+="?"+c.gc(b);if("img"in b){var f=n.createElement("img"); - f.src=a;n.body.appendChild(f)}else if(R)try{var j=new XMLHttpRequest;j.open("GET",a,l);j.withCredentials=l;j.onreadystatechange=function(){if(4===j.readyState)if(200===j.status){if(d)if(e){var a;try{a=c.la(j.responseText)}catch(b){o.error(b);return}d(a)}else d(Number(j.responseText))}else a="Bad HTTP status: "+j.status+" "+j.statusText,o.error(a),d&&(e?d({status:0,error:a}):d(0))};j.send(m)}catch(g){o.error(g)}else{f=n.createElement("script");f.type="text/javascript";f.async=l;f.defer=l;f.src=a;var i= - n.getElementsByTagName("script")[0];i.parentNode.insertBefore(f,i)}}};h.prototype.Ia=function(a){function b(a,b){c.a(a,function(a){this[a[0]].apply(this,a.slice(1))},b)}var d,e=[],f=[],j=[];c.a(a,function(a){a&&(d=a[0],"function"===typeof a?a.call(this):c.isArray(a)&&"alias"===d?e.push(a):c.isArray(a)&&-1!==d.indexOf("track")&&"function"===typeof this[d]?j.push(a):f.push(a))},this);b(e,this);b(f,this);b(j,this)};h.prototype.push=function(a){this.Ia([a])};h.prototype.disable=function(a){"undefined"=== - typeof a?this.Q.Oc=l:this.Fa=this.Fa.concat(a)};h.prototype.J=function(a,b,d){"function"!==typeof d&&(d=C());if(c.d(a))o.error("No event name provided to mixpanel.track");else if(this.mb(a))d(0);else{b=b||{};b.token=this.c("token");var e=this.persistence.fd(a);c.d(e)||(b.$duration=parseFloat((((new Date).getTime()-e)/1E3).toFixed(3)));this.persistence.bc(n.referrer);this.c("store_google")&&this.persistence.wd();this.c("save_referrer")&&this.persistence.hb(n.referrer);b=c.extend({},c.info.ga(),this.persistence.ga(), - b);try{if(this.c("autotrack")&&"mp_page_view"!==a&&"$create_alias"!==a)b=c.extend({},b,this.C),this.C={$__c:0},c.cookie.set("mp_"+this.c("name")+"__c",0,1,l)}catch(f){o.error(f)}e=this.c("property_blacklist");c.isArray(e)?c.a(e,function(a){delete b[a]}):o.error("Invalid value for property_blacklist config: "+e);a=c.truncate({event:a,properties:b},255);e=c.ma(a);e=c.xb(e);o.log("MIXPANEL REQUEST:");o.log(a);this.m(this.c("api_host")+"/track/",{data:e},this.ra(d,a));return a}};h.prototype.Yb=function(a){if(c.d(a))a= - n.location.href;this.J("mp_page_view",c.info.dd(a))};h.prototype.vd=function(){return this.Na.call(this,D,arguments)};h.prototype.ud=function(){return this.Na.call(this,L,arguments)};h.prototype.sd=function(a){c.d(a)?o.error("No event name provided to mixpanel.time_event"):this.mb(a)||this.persistence.nd(a)};h.prototype.ha=function(a,b){this.persistence.ha(a,b)};h.prototype.D=function(a,b,c){this.persistence.D(a,b,c)};h.prototype.Aa=function(a){this.persistence.Aa(a)};h.prototype.Ma=function(a,b){var c= - {};c[a]=b;this.ha(c)};h.prototype.Wa=function(a,b,c,e,f,j,g){a!==this.V()&&a!==this.va("__alias")&&(this.Aa("__alias"),this.Ma("distinct_id",a));this.jb(this.V());this.Q.Xa=l;this.people.qc(b,c,e,f,j,g)};h.prototype.reset=function(){this.persistence.clear();this.Q.Xa=s;this.D({distinct_id:c.ib()},"")};h.prototype.V=function(){return this.va("distinct_id")};h.prototype.Hc=function(a,b){if(a===this.va("$people_distinct_id"))return o.U("Attempting to create alias for existing People user - aborting."), - -2;var d=this;c.d(b)&&(b=this.V());if(a!==b)return this.Ma("__alias",a),this.J("$create_alias",{alias:a,distinct_id:b},function(){d.Wa(a)});o.error("alias matches current distinct_id - skipping api call.");this.Wa(a);return-1};h.prototype.bd=function(a){this.Ma("mp_name_tag",a)};h.prototype.Tb=function(a){if(c.k(a))c.extend(this.config,a),this.c("persistence_name")||(this.config.persistence_name=this.config.cookie_name),this.c("disable_persistence")||(this.config.disable_persistence=this.config.disable_cookie), - this.persistence&&this.persistence.ac(this.config),A=A||this.c("debug")};h.prototype.c=function(a){return this.config[a]};h.prototype.va=function(a){return this.persistence.props[a]};h.prototype.toString=function(){var a=this.c("name");"mixpanel"!==a&&(a="mixpanel."+a);return a};h.prototype.mb=function(a){return c.Lb(w)||this.Q.Oc||c.Ib(this.Fa,a)};h.prototype.jb=function(a){if(a&&!this.Q.Xa&&!this.c("disable_notifications")){o.log("MIXPANEL NOTIFICATION CHECK");var b=this;this.m(this.c("api_host")+ - "/decide/",{verbose:l,version:"2",lib:"web",token:this.c("token"),distinct_id:a},this.ra(function(a){a.notifications&&0");this.Mc=c.L(a.cta)||"Close";this.ea= - c.L(a.type)||"takeover";this.style=c.L(a.style)||"light";this.title=c.L(a.title)||"";this.ja=g.ic;this.Y=g.hc;this.ca=a.cta_url||m;this.Ya=a.image_url||m;this.M=a.thumb_image_url||m;this.Ba=a.video_url||m;this.ta=l;if(!this.ca)this.ca="#dismiss",this.ta=s;this.u="mini"===this.ea;if(!this.u)this.ea="takeover";this.cd=!this.u?g.Z:g.Ca;this.ub();this.wa=this.uc();this.xc()};g=h.jc;g.N=200;g.w="mixpanel-notification";g.ka=0.6;g.G=25;g.na=200;g.Z=388;g.Ca=420;g.z=85;g.Da=5;g.K=60;g.Ea=Math.round(g.K/2); - g.ic=595;g.hc=334;g.prototype.show=function(){var a=this;this.ub();this.q?(this.wc(),this.vc(),this.Bc(this.lc)):setTimeout(function(){a.show()},300)};g.prototype.Sa=c.l(function(){this.Mb||this.sb({invisible:l});var a=this.pd?this.h("video"):this.R();if(this.cc)this.Dc("bg","visible"),this.O(a,"exiting"),setTimeout(this.tb,g.N);else{var b,c,e;this.u?(b="right",c=20,e=-100):(b="top",c=g.G,e=g.na+g.G);this.oa([{s:this.h("bg"),o:"opacity",start:g.ka,j:0},{s:a,o:"opacity",start:1,j:0},{s:a,o:b,start:c, - j:e}],g.N,this.tb)}});g.prototype.O=c.l(function(a,b){b=g.w+"-"+b;"string"===typeof a&&(a=this.h(a));a.className?~(" "+a.className+" ").indexOf(" "+b+" ")||(a.className+=" "+b):a.className=b});g.prototype.Dc=c.l(function(a,b){b=g.w+"-"+b;"string"===typeof a&&(a=this.h(a));if(a.className)a.className=(" "+a.className+" ").replace(" "+b+" ","").replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")});g.prototype.oa=c.l(function(a,b,c,e){var f=this,g=s,h,i;h=1*new Date;var n,e=e||h;n=h-e;for(h=0;h=i.start?1:-1;i.F=i.start+(i.j-i.start)*n/b;if("opacity"!==i.o)i.F=Math.round(i.F);if(0=i.j||0>o&&i.F<=i.j)i.F=i.j}}if(g){for(h=0;h'):this.Hb="",this.M? - (a.push(this.M),this.Wb='
'):this.Wb="");return a};g.prototype.vc=function(){var a="",b="",c="";this.fa=n.createElement("div");this.fa.id=g.w+"-wrapper";if(this.u)a='
'+ - this.body+'
';else{var a=this.ta||this.X?"":'
',e=this.X?'
':"";this.P("ie",7)&&(e=a="");a='
'+this.Wb+'
'+this.Hb+'
'+this.title+'
'+this.body+'
'+ - a+''+this.Mc+""+e+"
"}this.ec?(b="//www.youtube.com/embed/"+this.ec+"?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080",this.fc&&(b+="&enablejsapi=1&html5=1&controls=0",c='
')): - this.dc&&(b="//player.vimeo.com/video/"+this.dc+"?autoplay=1&title=0&byline=0&portrait=0");if(this.X)this.Ad='',c='
'+c+"
";b=c+a;this.Va&&(b=(this.u?a:"")+'
'+(this.u?c:b)+"
"); - this.fa.innerHTML=('
'+b+"
").replace(/class=\"/g,'class="'+g.w+"-").replace(/id=\"/g,'id="'+g.w+"-")};g.prototype.wc=function(){this.g="dark"===this.style?{Qa:"#1d1f25",aa:"#282b32",sa:"#3a4147",yb:"#4a5157",Ic:"#32353c",Cb:"0.4",bb:"#2a3137",za:"#fff",gb:"#9498a3",Vb:"#464851",ya:"#ddd"}:{Qa:"#fff",aa:"#e7eaee",sa:"#eceff3",yb:"#f5f5f5",Ic:"#e4ecf2",Cb:"1.0",bb:"#fafafa", - za:"#5c6578",gb:"#8b949b",Vb:"#ced9e6",ya:"#7c8598"};var a="0px 0px 35px 0px rgba(45, 49, 56, 0.7)",b=a,d=a,e=g.K+2*g.Da,f=g.N/1E3+"s";this.u&&(a="none");var j={};j["@media only screen and (max-width: "+(g.Ca+20-1)+"px)"]={"#overlay":{display:"none"}};a={".flipped":{transform:"rotateY(180deg)"},"#overlay":{position:"fixed",top:"0",left:"0",width:"100%",height:"100%",overflow:"auto","text-align":"center","z-index":"10000","font-family":'"Helvetica", "Arial", sans-serif',"-webkit-font-smoothing":"antialiased", - "-moz-osx-font-smoothing":"grayscale"},"#overlay.mini":{height:"0",overflow:"visible"},"#overlay a":{width:"initial",padding:"0","text-decoration":"none","text-transform":"none",color:"inherit"},"#bgwrapper":{position:"relative",width:"100%",height:"100%"},"#bg":{position:"fixed",top:"0",left:"0",width:"100%",height:"100%","min-width":4*this.Qc+"px","min-height":4*this.Pc+"px","background-color":"black",opacity:"0.0","-ms-filter":"progid:DXImageTransform.Microsoft.Alpha(Opacity=60)",filter:"alpha(opacity=60)", - transition:"opacity "+f},"#bg.visible":{opacity:g.ka},".mini #bg":{width:"0",height:"0","min-width":"0"},"#flipcontainer":{perspective:"1000px",position:"absolute",width:"100%"},"#flipper":{position:"relative","transform-style":"preserve-3d",transition:"0.3s"},"#takeover":{position:"absolute",left:"50%",width:g.Z+"px","margin-left":Math.round(-g.Z/2)+"px","backface-visibility":"hidden",transform:"rotateY(0deg)",opacity:"0.0",top:g.na+"px",transition:"opacity "+f+", top "+f},"#takeover.visible":{opacity:"1.0", - top:g.G+"px"},"#takeover.exiting":{opacity:"0.0",top:g.na+"px"},"#thumbspacer":{height:g.Ea+"px"},"#thumbborder-wrapper":{position:"absolute",top:-g.Da+"px",left:g.Z/2-g.Ea-g.Da+"px",width:e+"px",height:e/2+"px",overflow:"hidden"},"#thumbborder":{position:"absolute",width:e+"px",height:e+"px","border-radius":e+"px","background-color":this.g.aa,opacity:"0.5"},"#thumbnail":{position:"absolute",top:"0px",left:g.Z/2-g.Ea+"px",width:g.K+"px",height:g.K+"px",overflow:"hidden","z-index":"100","border-radius":g.K+ - "px"},"#mini":{position:"absolute",right:"20px",top:g.G+"px",width:this.cd+"px",height:2*g.z+"px","margin-top":20-g.z+"px","backface-visibility":"hidden",opacity:"0.0",transform:"rotateX(90deg)",transition:"opacity 0.3s, transform 0.3s, right 0.3s"},"#mini.visible":{opacity:"1.0",transform:"rotateX(0deg)"},"#mini.exiting":{opacity:"0.0",right:"-150px"},"#mainbox":{"border-radius":"4px","box-shadow":a,"text-align":"center","background-color":this.g.Qa,"font-size":"14px",color:this.g.gb},"#mini #mainbox":{height:g.z+ - "px","margin-top":g.z+"px","border-radius":"3px",transition:"background-color "+f},"#mini-border":{height:g.z+6+"px",width:g.Ca+6+"px",position:"absolute",top:"-3px",left:"-3px","margin-top":g.z+"px","border-radius":"6px",opacity:"0.25","background-color":"#fff","z-index":"-1","box-shadow":d},"#mini-icon":{position:"relative",display:"inline-block",width:"75px",height:g.z+"px","border-radius":"3px 0 0 3px","background-color":this.g.aa,background:"linear-gradient(135deg, "+this.g.yb+" 0%, "+this.g.aa+ - " 100%)",transition:"background-color "+f},"#mini:hover #mini-icon":{"background-color":this.g.bb},"#mini:hover #mainbox":{"background-color":this.g.bb},"#mini-icon-img":{position:"absolute","background-image":"url("+this.M+")",width:"48px",height:"48px",top:"20px",left:"12px"},"#content":{padding:"30px 20px 0px 20px"},"#mini-content":{"text-align":"left",height:g.z+"px",cursor:"pointer"},"#img":{width:"328px","margin-top":"30px","border-radius":"5px"},"#title":{"max-height":"600px",overflow:"hidden", - "word-wrap":"break-word",padding:"25px 0px 20px 0px","font-size":"19px","font-weight":"bold",color:this.g.za},"#body":{"max-height":"600px","margin-bottom":"25px",overflow:"hidden","word-wrap":"break-word","line-height":"21px","font-size":"15px","font-weight":"normal","text-align":"left"},"#mini #body":{display:"inline-block","max-width":"250px",margin:"0 0 0 30px",height:g.z+"px","font-size":"16px","letter-spacing":"0.8px",color:this.g.za},"#mini #body-text":{display:"table",height:g.z+"px"},"#mini #body-text div":{display:"table-cell", - "vertical-align":"middle"},"#tagline":{"margin-bottom":"15px","font-size":"10px","font-weight":"600","letter-spacing":"0.8px",color:"#ccd7e0","text-align":"left"},"#tagline a":{color:this.g.Vb,transition:"color "+f},"#tagline a:hover":{color:this.g.ya},"#cancel":{position:"absolute",right:"0",width:"8px",height:"8px",padding:"10px","border-radius":"20px",margin:"12px 12px 0 0","box-sizing":"content-box",cursor:"pointer",transition:"background-color "+f},"#mini #cancel":{margin:"7px 7px 0 0"},"#cancel-icon":{width:"8px", - height:"8px",overflow:"hidden","background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)",opacity:this.g.Cb},"#cancel:hover":{"background-color":this.g.sa},"#button":{display:"block",height:"60px","line-height":"60px","text-align":"center","background-color":this.g.aa,"border-radius":"0 0 4px 4px",overflow:"hidden",cursor:"pointer",transition:"background-color "+f},"#button-close":{display:"inline-block",width:"9px",height:"60px","margin-right":"8px","vertical-align":"top", - "background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-"+this.style+".png)","background-repeat":"no-repeat","background-position":"0px 25px"},"#button-play":{display:"inline-block",width:"30px",height:"60px","margin-left":"15px","background-image":"url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-"+this.style+"-small.png)","background-repeat":"no-repeat","background-position":"0px 15px"},"a#button-link":{display:"inline-block","vertical-align":"top","text-align":"center", - "font-size":"17px","font-weight":"bold",overflow:"hidden","word-wrap":"break-word",color:this.g.za,transition:"color "+f},"#button:hover":{"background-color":this.g.sa,color:this.g.ya},"#button:hover a":{color:this.g.ya},"#video-noflip":{position:"relative",top:2*-this.Y+"px"},"#video-flip":{"backface-visibility":"hidden",transform:"rotateY(180deg)"},"#video":{position:"absolute",width:this.ja-1+"px",height:this.Y+"px",top:g.G+"px","margin-top":"100px",left:"50%","margin-left":Math.round(-this.ja/ - 2)+"px",overflow:"hidden","border-radius":"5px","box-shadow":b,transform:"translateZ(1px)",transition:"opacity "+f+", top "+f},"#video.exiting":{opacity:"0.0",top:this.Y+"px"},"#video-holder":{position:"absolute",width:this.ja-1+"px",height:this.Y+"px",overflow:"hidden","border-radius":"5px"},"#video-frame":{"margin-left":"-1px",width:this.ja+"px"},"#video-controls":{opacity:"0",transition:"opacity 0.5s"},"#video:hover #video-controls":{opacity:"1.0"},"#video .video-progress-el":{position:"absolute", - bottom:"0",height:"25px","border-radius":"0 0 0 5px"},"#video-progress":{width:"90%"},"#video-progress-total":{width:"100%","background-color":this.g.Qa,opacity:"0.7"},"#video-elapsed":{width:"0","background-color":"#6cb6f5",opacity:"0.9"},"#video #video-time":{width:"10%",right:"0","font-size":"11px","line-height":"25px",color:this.g.gb,"background-color":"#666","border-radius":"0 0 5px 0"}};this.P("ie",8)&&c.extend(a,{"* html #overlay":{position:"absolute"},"* html #bg":{position:"absolute"},"html, body":{height:"100%"}}); - this.P("ie",7)&&c.extend(a,{"#mini #body":{display:"inline",zoom:"1",border:"1px solid "+this.g.sa},"#mini #body-text":{padding:"20px"},"#mini #mini-icon":{display:"none"}});var b="backface-visibility,border-radius,box-shadow,opacity,perspective,transform,transform-style,transition".split(","),d=["khtml","moz","ms","o","webkit"],h;for(h in a)for(e=0;e(v.__SV||0)?o.U("Version mismatch; please ensure you're using the latest version of the Mixpanel code snippet."):(c.a(v._i,function(a){a&&c.isArray(a)&&(z[a[a.length-1]]=K.apply(this,a))}),ba(),v.init(),c.a(z,function(a){a.qa()}),aa())})()})(); +(function () { + 'use strict'; + + var Config = { + DEBUG: false, + LIB_VERSION: '2.19.0' + }; + + // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file + var win; + if (typeof(window) === 'undefined') { + var loc = { + hostname: '' + }; + win = { + navigator: { userAgent: '' }, + document: { + location: loc, + referrer: '' + }, + screen: { width: 0, height: 0 }, + location: loc + }; + } else { + win = window; + } + + + + /* + * Saved references to long variable names, so that closure compiler can + * minimize file size. + */ + + var ArrayProto = Array.prototype; + var FuncProto = Function.prototype; + var ObjProto = Object.prototype; + var slice = ArrayProto.slice; + var toString = ObjProto.toString; + var hasOwnProperty = ObjProto.hasOwnProperty; + var windowConsole = win.console; + var navigator$1 = win.navigator; + var document$1 = win.document; + var windowOpera = win.opera; + var screen = win.screen; + var userAgent = navigator$1.userAgent; + var nativeBind = FuncProto.bind; + var nativeForEach = ArrayProto.forEach; + var nativeIndexOf = ArrayProto.indexOf; + var nativeIsArray = Array.isArray; + var breaker = {}; + var _ = { + trim: function(str) { + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill + return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, ''); + } + }; + + // Console override + var console$1 = { + /** @type {function(...[*])} */ + log: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + try { + windowConsole.log.apply(windowConsole, arguments); + } catch (err) { + _.each(arguments, function(arg) { + windowConsole.log(arg); + }); + } + } + }, + /** @type {function(...[*])} */ + error: function() { + if (Config.DEBUG && !_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + }, + /** @type {function(...[*])} */ + critical: function() { + if (!_.isUndefined(windowConsole) && windowConsole) { + var args = ['Mixpanel error:'].concat(_.toArray(arguments)); + try { + windowConsole.error.apply(windowConsole, args); + } catch (err) { + _.each(args, function(arg) { + windowConsole.error(arg); + }); + } + } + } + }; + + + // UNDERSCORE + // Embed part of the Underscore Library + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) { + return nativeBind.apply(func, slice.call(arguments, 1)); + } + if (!_.isFunction(func)) { + throw new TypeError(); + } + args = slice.call(arguments, 2); + bound = function() { + if (!(this instanceof bound)) { + return func.apply(context, args.concat(slice.call(arguments))); + } + var ctor = {}; + ctor.prototype = func.prototype; + var self = new ctor(); + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) { + return result; + } + return self; + }; + return bound; + }; + + _.bind_instance_methods = function(obj) { + for (var func in obj) { + if (typeof(obj[func]) === 'function') { + obj[func] = _.bind(obj[func], obj); + } + } + }; + + /** + * @param {*=} obj + * @param {function(...[*])=} iterator + * @param {Object=} context + */ + _.each = function(obj, iterator, context) { + if (obj === null || obj === undefined) { + return; + } + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) { + return; + } + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) { + return; + } + } + } + } + }; + + _.escapeHTML = function(s) { + var escaped = s; + if (escaped && _.isString(escaped)) { + escaped = escaped + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return escaped; + }; + + _.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // from a comment on http://dbj.org/dbj/?p=286 + // fails on only one very rare and deliberate custom object: + // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; + _.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } + }; + + _.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); + }; + + _.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); + }; + + _.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; + }; + + _.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; + }; + + _.identity = function(value) { + return value; + }; + + _.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; + }; + + _.includes = function(str, needle) { + return str.indexOf(needle) !== -1; + }; + + // Underscore Addons + _.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; + }; + + _.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); + }; + + _.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; + }; + + _.isUndefined = function(obj) { + return obj === void 0; + }; + + _.isString = function(obj) { + return toString.call(obj) == '[object String]'; + }; + + _.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; + }; + + _.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; + }; + + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + _.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; + }; + + _.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); + }; + + _.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); + }; + + _.safewrap = function(f) { + return function() { + try { + return f.apply(this, arguments); + } catch (e) { + console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.'); + if (Config.DEBUG){ + console$1.critical(e); + } + } + }; + }; + + _.safewrap_class = function(klass, functions) { + for (var i = 0; i < functions.length; i++) { + klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]); + } + }; + + _.safewrap_instance_methods = function(obj) { + for (var func in obj) { + if (typeof(obj[func]) === 'function') { + obj[func] = _.safewrap(obj[func]); + } + } + }; + + _.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; + }; + + /* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ + _.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; + }; + + _.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; })(); + + /** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ + _.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; + })(); + + _.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; + }; + + _.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; + }; + + _.UUID = (function() { + + // Time/ticks information + // 1*new Date() is a cross browser version of Date.now() + var T = function() { + var d = 1 * new Date(), + i = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (d == 1 * new Date()) { + i++; + } + + return d.toString(16) + i.toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; + })(); + + // _.isBlockedUA() + // This is to block various web spiders from executing our JS and + // sending false tracking data + _.isBlockedUA = function(ua) { + if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) { + return true; + } + return false; + }; + + /** + * @param {Object=} formdata + * @param {string=} arg_separator + */ + _.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); + }; + + _.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + return decodeURIComponent(results[1]).replace(/\+/g, ' '); + } + }; + + _.getHashParam = function(hash, param) { + var matches = hash.match(new RegExp(param + '=([^&]*)')); + return matches ? matches[1] : null; + }; + + // _.cookie + // Methods partially borrowed from quirksmode.org/js/cookies.html + _.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, cross_subdomain, is_secure) { + var cdomain = '', + expires = '', + secure = ''; + + if (cross_subdomain) { + var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), + domain = matches ? matches[0] : ''; + + cdomain = ((domain) ? '; domain=.' + domain : ''); + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_secure) { + secure = '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, cross_subdomain, is_secure) { + var cdomain = '', expires = '', secure = ''; + + if (cross_subdomain) { + var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), + domain = matches ? matches[0] : ''; + + cdomain = ((domain) ? '; domain=.' + domain : ''); + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_secure) { + secure = '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, cross_subdomain) { + _.cookie.set(name, '', -1, cross_subdomain); + } + }; + + // _.localStorage + _.localStorage = { + error: function(msg) { + console$1.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } + }; + + _.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...[*])} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console$1.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; + })(); + + _.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; + })(); + + _.info = { + campaignParams: function() { + var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' '), + kw = '', + params = {}; + _.each(campaign_keywords, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } + }); + + return params; + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'Edge')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': win.location.href, + '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION + }); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera) + }); + }, + + pageviewInfo: function(page) { + return _.strip_empty_properties({ + 'mp_page': page, + 'mp_referrer': document$1.referrer, + 'mp_browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), + 'mp_platform': _.info.os() + }); + } + }; + + // EXPORTS (for closure compiler) + _['toArray'] = _.toArray; + _['isObject'] = _.isObject; + _['JSONEncode'] = _.JSONEncode; + _['JSONDecode'] = _.JSONDecode; + _['isBlockedUA'] = _.isBlockedUA; + _['isEmptyObject'] = _.isEmptyObject; + _['info'] = _.info; + _['info']['device'] = _.info.device; + _['info']['browser'] = _.info.browser; + _['info']['properties'] = _.info.properties; + + /* + * Get the className of an element, accounting for edge cases where element.className is an object + * @param {Element} el - element to get the className of + * @returns {string} the element's class + */ + function getClassName(el) { + switch(typeof el.className) { + case 'string': + return el.className; + case 'object': // handle cases where className might be SVGAnimatedString or some other type + return el.className.baseVal || el.getAttribute('class') || ''; + default: // future proof + return ''; + } + } + + /* + * Get the direct text content of an element, protecting against sensitive data collection. + * Concats textContent of each of the element's text node children; this avoids potential + * collection of sensitive data that could happen if we used element.textContent and the + * element had sensitive child elements, since element.textContent includes child content. + * Scrubs values that look like they could be sensitive (i.e. cc or ssn number). + * @param {Element} el - element to get the text of + * @returns {string} the element's direct text content + */ + function getSafeText(el) { + var elText = ''; + + if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) { + _.each(el.childNodes, function(child) { + if (isTextNode(child) && child.textContent) { + elText += _.trim(child.textContent) + // scrub potentially sensitive values + .split(/(\s+)/).filter(shouldTrackValue).join('') + // normalize whitespace + .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ') + // truncate + .substring(0, 255); + } + }); + } + + return _.trim(elText); + } + + /* + * Check whether an element has nodeType Node.ELEMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ + function isElementNode(el) { + return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability + } + + /* + * Check whether an element is of a given tag type. + * Due to potential reference discrepancies (such as the webcomponents.js polyfill), + * we want to match tagNames instead of specific references because something like + * element === document.body won't always work because element might not be a native + * element. + * @param {Element} el - element to check + * @param {string} tag - tag name (e.g., "div") + * @returns {boolean} whether el is of the given tag type + */ + function isTag(el, tag) { + return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase(); + } + + /* + * Check whether an element has nodeType Node.TEXT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ + function isTextNode(el) { + return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability + } + + /* + * Check whether a DOM event should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {Element} el - element to check + * @param {Event} event - event to check + * @returns {boolean} whether the event should be tracked + */ + function shouldTrackDomEvent(el, event) { + if (!el || isTag(el, 'html') || !isElementNode(el)) { + return false; + } + var tag = el.tagName.toLowerCase(); + switch (tag) { + case 'html': + return false; + case 'form': + return event.type === 'submit'; + case 'input': + if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) { + return event.type === 'change'; + } else { + return event.type === 'click'; + } + case 'select': + case 'textarea': + return event.type === 'change'; + default: + return event.type === 'click'; + } + } + + /* + * Check whether a DOM element should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {Element} el - element to check + * @returns {boolean} whether the element should be tracked + */ + function shouldTrackElement(el) { + for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) { + var classes = getClassName(curEl).split(' '); + if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) { + return false; + } + } + + if (_.includes(getClassName(el).split(' '), 'mp-include')) { + return true; + } + + // don't send data from inputs or similar elements since there will always be + // a risk of clientside javascript placing sensitive data in attributes + if ( + isTag(el, 'input') || + isTag(el, 'select') || + isTag(el, 'textarea') || + el.getAttribute('contenteditable') === 'true' + ) { + return false; + } + + // don't include hidden or password fields + var type = el.type || ''; + if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"] + switch(type.toLowerCase()) { + case 'hidden': + return false; + case 'password': + return false; + } + } + + // filter out data from fields that look like sensitive fields + var name = el.name || el.id || ''; + if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"] + var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i; + if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) { + return false; + } + } + + return true; + } + + /* + * Check whether a string value should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {string} value - string value to check + * @returns {boolean} whether the element should be tracked + */ + function shouldTrackValue(value) { + if (value === null || _.isUndefined(value)) { + return false; + } + + if (typeof value === 'string') { + value = _.trim(value); + + // check to see if input value looks like a credit card number + // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html + var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/; + if (ccRegex.test((value || '').replace(/[\- ]/g, ''))) { + return false; + } + + // check to see if input value looks like a social security number + var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/; + if (ssnRegex.test(value)) { + return false; + } + } + + return true; + } + + var autotrack = { + _initializedTokens: [], + + _previousElementSibling: function(el) { + if (el.previousElementSibling) { + return el.previousElementSibling; + } else { + do { + el = el.previousSibling; + } while (el && !isElementNode(el)); + return el; + } + }, + + _loadScript: function(scriptUrlToLoad, callback) { + var scriptTag = document.createElement('script'); + scriptTag.type = 'text/javascript'; + scriptTag.src = scriptUrlToLoad; + scriptTag.onload = callback; + + var scripts = document.getElementsByTagName('script'); + if (scripts.length > 0) { + scripts[0].parentNode.insertBefore(scriptTag, scripts[0]); + } else { + document.body.appendChild(scriptTag); + } + }, + + _getPropertiesFromElement: function(elem) { + var props = { + 'classes': getClassName(elem).split(' '), + 'tag_name': elem.tagName.toLowerCase() + }; + + if (shouldTrackElement(elem)) { + _.each(elem.attributes, function(attr) { + if (shouldTrackValue(attr.value)) { + props['attr__' + attr.name] = attr.value; + } + }); + } + + var nthChild = 1; + var nthOfType = 1; + var currentElem = elem; + while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign + nthChild++; + if (currentElem.tagName === elem.tagName) { + nthOfType++; + } + } + props['nth_child'] = nthChild; + props['nth_of_type'] = nthOfType; + + return props; + }, + + _getDefaultProperties: function(eventType) { + return { + '$event_type': eventType, + '$ce_version': 1, + '$host': window.location.host, + '$pathname': window.location.pathname + }; + }, + + _extractCustomPropertyValue: function(customProperty) { + var propValues = []; + _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) { + var value; + + if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) { + value = matchedElem['value']; + } else if (matchedElem['textContent']) { + value = matchedElem['textContent']; + } + + if (shouldTrackValue(value)) { + propValues.push(value); + } + }); + return propValues.join(', '); + }, + + _getCustomProperties: function(targetElementList) { + var props = {}; + _.each(this._customProperties, function(customProperty) { + _.each(customProperty['event_selectors'], function(eventSelector) { + var eventElements = document.querySelectorAll(eventSelector); + _.each(eventElements, function(eventElement) { + if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) { + props[customProperty['name']] = this._extractCustomPropertyValue(customProperty); + } + }, this); + }, this); + }, this); + return props; + }, + + _getEventTarget: function(e) { + // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes + if (typeof e.target === 'undefined') { + return e.srcElement; + } else { + return e.target; + } + }, + + _trackEvent: function(e, instance) { + /*** Don't mess with this code without running IE8 tests on it ***/ + var target = this._getEventTarget(e); + if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html) + target = target.parentNode; + } + + if (shouldTrackDomEvent(target, e)) { + var targetElementList = [target]; + var curEl = target; + while (curEl.parentNode && !isTag(curEl, 'body')) { + targetElementList.push(curEl.parentNode); + curEl = curEl.parentNode; + } + + var elementsJson = []; + var href, explicitNoTrack = false; + _.each(targetElementList, function(el) { + var shouldTrackEl = shouldTrackElement(el); + + // if the element or a parent element is an anchor tag + // include the href as a property + if (el.tagName.toLowerCase() === 'a') { + href = el.getAttribute('href'); + href = shouldTrackEl && shouldTrackValue(href) && href; + } + + // allow users to programatically prevent tracking of elements by adding class 'mp-no-track' + var classes = getClassName(el).split(' '); + if (_.includes(classes, 'mp-no-track')) { + explicitNoTrack = true; + } + + elementsJson.push(this._getPropertiesFromElement(el)); + }, this); + + if (explicitNoTrack) { + return false; + } + + // only populate text content from target element (not parents) + // to prevent text within a sensitive element from being collected + // as part of a parent's el.textContent + var elementText; + var safeElementText = getSafeText(target); + if (safeElementText && safeElementText.length) { + elementText = safeElementText; + } + + var props = _.extend( + this._getDefaultProperties(e.type), + { + '$elements': elementsJson, + '$el_attr__href': href, + '$el_text': elementText + }, + this._getCustomProperties(targetElementList) + ); + + instance.track('$web_event', props); + return true; + } + }, + + // only reason is to stub for unit tests + // since you can't override window.location props + _navigate: function(href) { + window.location.href = href; + }, + + _addDomEventHandlers: function(instance) { + var handler = _.bind(function(e) { + e = e || window.event; + this._trackEvent(e, instance); + }, this); + _.register_event(document, 'submit', handler, false, true); + _.register_event(document, 'change', handler, false, true); + _.register_event(document, 'click', handler, false, true); + }, + + _customProperties: {}, + init: function(instance) { + if (!(document && document.body)) { + console.log('document not ready yet, trying again in 500 milliseconds...'); + var that = this; + setTimeout(function() { that.init(instance); }, 500); + return; + } + + var token = instance.get_config('token'); + if (this._initializedTokens.indexOf(token) > -1) { + console.log('autotrack already initialized for token "' + token + '"'); + return; + } + this._initializedTokens.push(token); + + if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled + var parseDecideResponse = _.bind(function(response) { + if (response && response['config'] && response['config']['enable_collect_everything'] === true) { + + if (response['custom_properties']) { + this._customProperties = response['custom_properties']; + } + + instance.track('$web_event', _.extend({ + '$title': document.title + }, this._getDefaultProperties('pageview'))); + + this._addDomEventHandlers(instance); + + } else { + instance['__autotrack_enabled'] = false; + } + }, this); + + instance._send_request( + instance.get_config('api_host') + '/decide/', { + 'verbose': true, + 'version': '1', + 'lib': 'web', + 'token': token + }, + instance._prepare_callback(parseDecideResponse) + ); + } + }, + + _editorParamsFromHash: function(instance, hash) { + var editorParams; + try { + var state = _.getHashParam(hash, 'state'); + state = JSON.parse(decodeURIComponent(state)); + var expiresInSeconds = _.getHashParam(hash, 'expires_in'); + editorParams = { + 'accessToken': _.getHashParam(hash, 'access_token'), + 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000), + 'bookmarkletMode': !!state['bookmarkletMode'], + 'projectId': state['projectId'], + 'projectOwnerId': state['projectOwnerId'], + 'projectToken': state['token'], + 'readOnly': state['readOnly'], + 'userFlags': state['userFlags'], + 'userId': state['userId'] + }; + window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams)); + + if (state['desiredHash']) { + window.location.hash = state['desiredHash']; + } else if (window.history) { + history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash + } else { + window.location.hash = ''; // clear hash (but leaves # unfortunately) + } + } catch (e) { + console.error('Unable to parse data from hash', e); + } + return editorParams; + }, + + /** + * To load the visual editor, we need an access token and other state. That state comes from one of three places: + * 1. In the URL hash params if the customer is using an old snippet + * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash + * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page + */ + _maybeLoadEditor: function(instance) { + try { + var parseFromUrl = false; + if (_.getHashParam(window.location.hash, 'state')) { + var state = _.getHashParam(window.location.hash, 'state'); + state = JSON.parse(decodeURIComponent(state)); + parseFromUrl = state['action'] === 'mpeditor'; + } + var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash'); + var editorParams; + + if (parseFromUrl) { // happens if they are initializing the editor using an old snippet + editorParams = this._editorParamsFromHash(instance, window.location.hash); + } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet + editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash')); + window.sessionStorage.removeItem('_mpcehash'); + } else { // get credentials from sessionStorage from a previous initialzation + editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}'); + } + + if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) { + this._loadEditor(instance, editorParams); + return true; + } else { + return false; + } + } catch (e) { + return false; + } + }, + + _loadEditor: function(instance, editorParams) { + if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib + window['_mpEditorLoaded'] = true; + var editorUrl = instance.get_config('app_host') + + '/js-bundle/reports/collect-everything/editor.js?_ts=' + + (new Date()).getTime(); + this._loadScript(editorUrl, function() { + window['mp_load_editor'](editorParams); + }); + return true; + } + return false; + }, + + // this is a mechanism to ramp up CE with no server-side interaction. + // when CE is active, every page load results in a decide request. we + // need to gently ramp this up so we don't overload decide. this decides + // deterministically if CE is enabled for this project by modding the char + // value of the project token. + enabledForProject: function(token, numBuckets, numEnabledBuckets) { + numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10; + numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10; + var charCodeSum = 0; + for (var i = 0; i < token.length; i++) { + charCodeSum += token.charCodeAt(i); + } + return (charCodeSum % numBuckets) < numEnabledBuckets; + }, + + isBrowserSupported: function() { + return _.isFunction(document.querySelectorAll); + } + }; + + _.bind_instance_methods(autotrack); + _.safewrap_instance_methods(autotrack); + + /* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + + // ==ClosureCompiler== + // @compilation_level ADVANCED_OPTIMIZATIONS + // @output_file_name mixpanel-2.8.min.js + // ==/ClosureCompiler== + + /* + SIMPLE STYLE GUIDE: + + this.x === public function + this._x === internal - only use within this file + this.__x === private - only use within the class + + Globals should be all caps + */ + + var init_type; // MODULE or SNIPPET loader + var mixpanel_master; // main mixpanel instance / object + var INIT_MODULE = 0; + var INIT_SNIPPET = 1; + + /* + * Constants + */ + /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; + /** @const */ var SET_QUEUE_KEY = '__mps'; + /** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; + /** @const */ var UNSET_QUEUE_KEY = '__mpus'; + /** @const */ var ADD_QUEUE_KEY = '__mpa'; + /** @const */ var APPEND_QUEUE_KEY = '__mpap'; + /** @const */ var UNION_QUEUE_KEY = '__mpu'; + /** @const */ var SET_ACTION = '$set'; + /** @const */ var SET_ONCE_ACTION = '$set_once'; + /** @const */ var UNSET_ACTION = '$unset'; + /** @const */ var ADD_ACTION = '$add'; + /** @const */ var APPEND_ACTION = '$append'; + /** @const */ var UNION_ACTION = '$union'; + // This key is deprecated, but we want to check for it to see whether aliasing is allowed. + /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; + /** @const */ var ALIAS_ID_KEY = '__alias'; + /** @const */ var CAMPAIGN_IDS_KEY = '__cmpns'; + /** @const */ var EVENT_TIMERS_KEY = '__timers'; + /** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + CAMPAIGN_IDS_KEY, + EVENT_TIMERS_KEY + ]; + + /* + * Dynamic... constants? Is that an oxymoron? + */ + // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ + // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials + var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + + // IE<10 does not support cross-origin XHR's but script tags + // with defer won't block window.onload; ENQUEUE_REQUESTS + // should only be true for Opera<12 + var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + + /* + * Module-level globals + */ + var DEFAULT_CONFIG = { + 'api_host': 'https://api.mixpanel.com', + 'app_host': 'https://mixpanel.com', + 'autotrack': true, + 'cdn': 'https://cdn.mxpnl.com', + 'cross_subdomain_cookie': true, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_name': '', + 'loaded': function() {}, + 'store_google': true, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'track_pageview': true, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'property_blacklist': [] + }; + + var DOM_LOADED = false; + + /** + * DomTracker Object + * @constructor + */ + var DomTracker = function() {}; + + // interface + DomTracker.prototype.create_properties = function() {}; + DomTracker.prototype.event_handler = function() {}; + DomTracker.prototype.after_track_handler = function() {}; + + DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; + }; + + /** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function(...[*])=} user_callback + */ + DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + win.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; + }; + + /** + * @param {function(...[*])} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ + DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; + }; + + DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; + }; + + /** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ + var LinkTracker = function() { + this.override_event = 'click'; + }; + _.inherit(LinkTracker, DomTracker); + + LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; + }; + + LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } + }; + + LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + win.location = options.href; + }, 0); + }; + + /** + * FormTracker Object + * @constructor + * @extends DomTracker + */ + var FormTracker = function() { + this.override_event = 'submit'; + }; + _.inherit(FormTracker, DomTracker); + + FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); + }; + + FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); + }; + + /** + * Mixpanel Persistence Object + * @constructor + */ + var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + var localStorage_supported = function() { + var supported = true; + try { + var key = '__mplssupport__', + val = 'xyz'; + _.localStorage.set(key, val); + if (_.localStorage.get(key) !== val) { + supported = false; + } + _.localStorage.remove(key); + } catch (err) { + supported = false; + } + if (!supported) { + console$1.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }; + if (storage_type === 'localStorage' && localStorage_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(config); + this.save(); + }; + + MixpanelPersistence.prototype.properties = function() { + var p = {}; + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; + }; + + MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } + }; + + MixpanelPersistence.prototype.upgrade = function(config) { + var upgrade_from_old_lib = config['upgrade'], + old_cookie_name, + old_cookie; + + if (upgrade_from_old_lib) { + old_cookie_name = 'mp_super_properties'; + // Case where they had a custom cookie name before. + if (typeof(upgrade_from_old_lib) === 'string') { + old_cookie_name = upgrade_from_old_lib; + } + + old_cookie = this.storage.parse(old_cookie_name); + + // remove the cookie + this.storage.remove(old_cookie_name); + this.storage.remove(old_cookie_name, true); + + if (old_cookie) { + this['props'] = _.extend( + this['props'], + old_cookie['all'], + old_cookie['events'] + ); + } + } + + if (!config['cookie_name'] && config['name'] !== 'mixpanel') { + // special case to handle people with cookies of the form + // mp_TOKEN_INSTANCENAME from the first release of this library + old_cookie_name = 'mp_' + config['token'] + '_' + config['name']; + old_cookie = this.storage.parse(old_cookie_name); + + if (old_cookie) { + this.storage.remove(old_cookie_name); + this.storage.remove(old_cookie_name, true); + + // Save the prop values that were in the cookie from before - + // this should only happen once as we delete the old one. + this.register_once(old_cookie); + } + } + + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } + }; + + MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + this._expire_notification_campaigns(); + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure + ); + }; + + MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false); + this.storage.remove(this.name, true); + }; + + // removes the storage entry and deletes all loaded data + // forced name for tests + MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; + }; + + /** + * @param {Object} props + * @param {*=} default_value + * @param {number=} days + */ + MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; + }; + + /** + * @param {Object} props + * @param {number=} days + */ + MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + _.extend(this['props'], props); + + this.save(); + + return true; + } + return false; + }; + + MixpanelPersistence.prototype.unregister = function(prop) { + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } + }; + + MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() { + var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY], + EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN) + if (!campaigns_shown) { + return; + } + for (var campaign_id in campaigns_shown) { + if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) { + delete campaigns_shown[campaign_id]; + } + } + if (_.isEmptyObject(campaigns_shown)) { + delete this['props'][CAMPAIGN_IDS_KEY]; + } + }); + + MixpanelPersistence.prototype.update_campaign_params = function() { + if (!this.campaign_params_saved) { + this.register_once(_.info.campaignParams()); + this.campaign_params_saved = true; + } + }; + + MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); + }; + + // EXPORTED METHOD, we test this directly. + MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); + }; + + MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); + }; + + // safely fills the passed in object with stored properties, + // does not override any properties defined in both + // returns the passed in object + MixpanelPersistence.prototype.safe_merge = function(props) { + _.each(this['props'], function(val, prop) { + if (!(prop in props)) { + props[prop] = val; + } + }); + + return props; + }; + + MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); + }; + + MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } + }; + + MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } + }; + + MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; + }; + + MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } + }; + + MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); + + this.save(); + }; + + MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this._get_queue(queue); + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + delete q[k]; + }, this); + + this.save(); + } + }; + + MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console$1.error('Invalid queue:', queue); + } + }; + + MixpanelPersistence.prototype._get_queue = function(queue) { + return this['props'][this._get_queue_key(queue)]; + }; + MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + + return this['props'][key] || (this['props'][key] = default_val); + }; + + MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this['props'][EVENT_TIMERS_KEY] || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); + }; + + MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this['props'][EVENT_TIMERS_KEY] || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; + }; + + /** + * Mixpanel Library Object + * @constructor + */ + var MixpanelLib = function() {}; + + /** + * Mixpanel People Object + * @constructor + */ + var MixpanelPeople = function() {}; + + var MPNotif; + + /** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ + var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console$1.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + instance['__autotrack_enabled'] = instance.get_config('autotrack'); + if (instance.get_config('autotrack')) { + var num_buckets = 100; + var num_enabled_buckets = 100; + if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) { + instance['__autotrack_enabled'] = false; + console$1.log('Not in active bucket: disabling Automatic Event Collection.'); + } else if (!autotrack.isBrowserSupported()) { + instance['__autotrack_enabled'] = false; + console$1.log('Disabling Automatic Event Collection because this browser is not supported'); + } else { + autotrack.init(instance); + } + + try { + add_dom_event_counting_handlers(instance); + } catch (e) { + console$1.error(e); + } + } + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; + }; + + // Initialization methods + + /** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ + MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + console$1.error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; + }; + + // mixpanel._init(token:string, config:object, name:string) + // + // This function sets up the current instance of the mixpanel + // library. The difference between this method and the init(...) + // method is this one initializes the actual instance, whereas the + // init(...) method sets up a new library and calls _init on it. + // + MixpanelLib.prototype._init = function(token, config, name) { + this['__loaded'] = true; + this['config'] = {}; + + this.set_config(_.extend({}, DEFAULT_CONFIG, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = function() {}; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.register_once({'distinct_id': _.UUID()}, ''); + }; + + // Private methods + + MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + + // this happens after so a user can call identify/name_tag in + // the loaded callback + if (this.get_config('track_pageview')) { + this.track_pageview(); + } + }; + + MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + delete this.__dom_loaded_queue; + delete this.__request_queue; + }; + + MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + console$1.error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); + }; + + /** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ + MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } + }; + + MixpanelLib.prototype._send_request = function(url, data, callback) { + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return; + } + + // needed to correctly format responses + var verbose_mode = this.get_config('verbose'); + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + url += '?' + _.HTTPBuildQuery(data); + + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open('GET', url, true); + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + console$1.error(e); + return; + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + console$1.error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error}); + } else { + callback(0); + } + } + } + } + }; + req.send(null); + } catch (e) { + console$1.error(e); + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + }; + + /** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ + MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + this[item[0]].apply(this, item.slice(1)); + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); + }; + + /** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ + MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); + }; + + /** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ + MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } + }; + + /** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + MixpanelLib.prototype.track = function(event_name, properties, callback) { + if (typeof(callback) !== 'function') { + callback = function() {}; + } + + if (_.isUndefined(event_name)) { + console$1.error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = properties || {}; + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + // update persistence + this['persistence'].update_search_keyword(document$1.referrer); + + if (this.get_config('store_google')) { this['persistence'].update_campaign_params(); } + if (this.get_config('save_referrer')) { this['persistence'].update_referrer_info(document$1.referrer); } + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties(), + this['persistence'].properties(), + properties + ); + + try { + if (this.get_config('autotrack') && event_name !== 'mp_page_view' && event_name !== '$create_alias') { + // The point of $__c is to count how many clicks occur per tracked event. Since we're + // tracking an event in this function, we need to reset the $__c value. + properties = _.extend({}, properties, this.mp_counts); + this.mp_counts = {'$__c': 0}; + _.cookie.set('mp_' + this.get_config('name') + '__c', 0, 1, true); + } + } catch (e) { + console$1.error(e); + } + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + console$1.error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + + var truncated_data = _.truncate(data, 255); + var json_data = _.JSONEncode(truncated_data); + var encoded_data = _.base64Encode(json_data); + + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); + + this._send_request( + this.get_config('api_host') + '/track/', + { 'data': encoded_data }, + this._prepare_callback(callback, truncated_data) + ); + + return truncated_data; + }; + + /** + * Track a page view event, which is currently ignored by the server. + * This function is called by default on page load unless the + * track_pageview configuration variable is false. + * + * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. + * @api private + */ + MixpanelLib.prototype.track_pageview = function(page) { + if (_.isUndefined(page)) { + page = document$1.location.href; + } + this.track('mp_page_view', _.info.pageviewInfo(page)); + }; + + /** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ + MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); + }; + + /** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ + MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); + }; + + /** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ + MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + console$1.error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); + }; + + /** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number} [days] How many days since the user's last visit to store the super properties + */ + MixpanelLib.prototype.register = function(props, days) { + this['persistence'].register(props, days); + }; + + /** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number} [days] How many days since the users last visit to store the super properties + */ + MixpanelLib.prototype.register_once = function(props, default_value, days) { + this['persistence'].register_once(props, default_value, days); + }; + + /** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + */ + MixpanelLib.prototype.unregister = function(property) { + this['persistence'].unregister(property); + }; + + MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); + }; + + /** + * Identify a user with a unique ID instead of a Mixpanel + * randomly generated distinct_id. If the method is never called, + * then unique visitors will be identified by a UUID generated + * the first time they visit the site. + * + * ### Notes: + * + * You can call this function to overwrite a previously set + * unique ID for the current user. Mixpanel cannot translate + * between IDs at this time, so when you change a user's ID + * they will appear to be a new user. + * + * When used alone, mixpanel.identify will change the user's + * distinct_id to the unique ID provided. When used in tandem + * with mixpanel.alias, it will allow you to identify based on + * unique ID and map that back to the original, anonymous + * distinct_id given to the user upon her first arrival to your + * site (thus connecting anonymous pre-signup activity to + * post-signup activity). Though the two work together, do not + * call identify() at the same time as alias(). Calling the two + * at the same time can cause a race condition, so it is best + * practice to call identify on the original, anonymous ID + * right after you've aliased it. + * Learn more about how mixpanel.identify and mixpanel.alias can be used. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ + MixpanelLib.prototype.identify = function( + unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback + ) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (unique_id !== this.get_distinct_id() && unique_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this._register_single('distinct_id', unique_id); + } + this._check_and_handle_notifications(this.get_distinct_id()); + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback); + }; + + /** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ + MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + this.register_once({'distinct_id': _.UUID()}, ''); + }; + + /** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ + MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); + }; + + /** + * Create an alias, which Mixpanel will use to link two distinct_ids going forward (not retroactively). + * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the + * following is a valid scenario: + * + * mixpanel.alias('new_id', 'existing_id'); + * ... + * mixpanel.alias('newer_id', 'new_id'); + * + * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. + * + * ### Notes: + * + * The best practice is to call alias() when a unique ID is first created for a user + * (e.g., when a user first registers for an account and provides an email address). + * alias() should never be called more than once for a given user, except to + * chain a newer ID to a previously new ID, as described above. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ + MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + console$1.critical('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + console$1.error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } + }; + + /** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @api private + */ + MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); + }; + + /** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // should we track a page view on page load + * track_pageview: true + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ + MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } + }; + + /** + * returns the current config object for the library. + */ + MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; + }; + + /** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ + MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence']['props'][property_name]; + }; + + MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; + }; + + MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); + }; + + MixpanelLib.prototype._check_and_handle_notifications = function(distinct_id) { + if (!distinct_id || this._flags.identify_called || this.get_config('disable_notifications')) { + return; + } + + console$1.log('MIXPANEL NOTIFICATION CHECK'); + + var data = { + 'verbose': true, + 'version': '2', + 'lib': 'web', + 'token': this.get_config('token'), + 'distinct_id': distinct_id + }; + var self = this; + this._send_request( + this.get_config('api_host') + '/decide/', + data, + this._prepare_callback(function(r) { + if (r['notifications'] && r['notifications'].length > 0) { + self._show_notification.call(self, r['notifications'][0]); + } + }) + ); + }; + + MixpanelLib.prototype._show_notification = function(notification_data) { + var notification = new MPNotif(notification_data, this); + notification.show(); + }; + + MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; + }; + + /* + * Set properties on a user record. + * + * ### Usage: + * + * mixpanel.people.set('gender', 'm'); + * + * // or set multiple properties at once + * mixpanel.people.set({ + * 'Company': 'Acme', + * 'Plan': 'Premium', + * 'Upgrade date': new Date() + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.set = function(prop, to, callback) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + callback = to; + } else { + $set[prop] = to; + } + + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document$1.referrer); + } + + // update $set object with default people properties + $set = _.extend( + {}, + _.info.people_properties(), + this._mixpanel['persistence'].get_referrer_info(), + $set + ); + + data[SET_ACTION] = $set; + + return this._send_request(data, callback); + }; + + /* + * Set properties on a user record, only if they do not yet exist. + * This will not overwrite previous people property values, unlike + * people.set(). + * + * ### Usage: + * + * mixpanel.people.set_once('First Login Date', new Date()); + * + * // or set multiple properties at once + * mixpanel.people.set_once({ + * 'First Login Date': new Date(), + * 'Starting Plan': 'Premium' + * }); + * + * // properties can be strings, integers or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.set_once = function(prop, to, callback) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + callback = to; + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return this._send_request(data, callback); + }; + + /* + * Unset properties on a user record (permanently removes the properties and their values from a profile). + * + * ### Usage: + * + * mixpanel.people.unset('gender'); + * + * // or unset multiple properties at once + * mixpanel.people.unset(['gender', 'Company']); + * + * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.unset = function(prop, callback) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + + return this._send_request(data, callback); + }; + + /* + * Increment/decrement numeric people analytics properties. + * + * ### Usage: + * + * mixpanel.people.increment('page_views', 1); + * + * // or, for convenience, if you're just incrementing a counter by + * // 1, you can simply do + * mixpanel.people.increment('page_views'); + * + * // to decrement a counter, pass a negative number + * mixpanel.people.increment('credits_left', -1); + * + * // like mixpanel.people.set(), you can increment multiple + * // properties at once: + * mixpanel.people.increment({ + * counter1: 1, + * counter2: 6 + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. + * @param {Number} [by] An amount to increment the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.increment = function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); + }; + + /* + * Append a value to a list-valued people analytics property. + * + * ### Usage: + * + * // append a value to a list, creating it if needed + * mixpanel.people.append('pages_visited', 'homepage'); + * + * // like mixpanel.people.set(), you can append multiple + * // properties at once: + * mixpanel.people.append({ + * list1: 'bob', + * list2: 123 + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [value] An item to append to the list + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.append = function(list_name, value, callback) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + callback = value; + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + + return this._send_request(data, callback); + }; + + /* + * Merge a given list with a list-valued people analytics property, + * excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.people.union('pages_visited', 'homepage'); + * + * // like mixpanel.people.set(), you can append multiple + * // properties at once: + * mixpanel.people.union({ + * list1: 'bob', + * list2: 123 + * }); + * + * // like mixpanel.people.append(), you can append multiple + * // values to the same list: + * mixpanel.people.union({ + * list1: ['bob', 'billy'] + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [value] Value / values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.union = function(list_name, values, callback) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + callback = values; + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + + return this._send_request(data, callback); + }; + + /* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + */ + MixpanelPeople.prototype.track_charge = function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); + }; + + /* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); + }; + + /* + * Permanently deletes the current people analytics profile from + * Mixpanel (using the current distinct_id). + * + * ### Usage: + * + * // remove the all data you have stored about the current user + * mixpanel.people.delete_user(); + * + */ + MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); + }; + + MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; + }; + + MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + + var date_encoded_data = _.encodeDates(data); + var truncated_data = _.truncate(date_encoded_data, 255); + var json_data = _.JSONEncode(date_encoded_data); + var encoded_data = _.base64Encode(json_data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return truncated_data; + } + + console$1.log('MIXPANEL PEOPLE REQUEST:'); + console$1.log(truncated_data); + + this._mixpanel._send_request( + this._get_config('api_host') + '/engage/', + {'data': encoded_data}, + this._mixpanel._prepare_callback(callback, truncated_data) + ); + + return truncated_data; + }; + + MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); + }; + + MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; + }; + + // Queue up engage operations if identify hasn't been called yet. + MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console$1.error('Invalid call to _enqueue():', data); + } + }; + + MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } + }; + + // Flush queued engage operations - order does not matter, + // and there are network level race conditions anyway + MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback + ) { + var _this = this; + var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION); + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_item = $append_queue.pop(); + _this.append($append_item, callback); + } + // Save the shortened append queue + _this._mixpanel['persistence'].save(); + } + }; + + MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token'; + }; + + + // Internal class for notification display + MixpanelLib._Notification = function(notif_data, mixpanel_instance) { + _.bind_instance_methods(this); + + this.mixpanel = mixpanel_instance; + this.persistence = this.mixpanel['persistence']; + + this.campaign_id = _.escapeHTML(notif_data['id']); + this.message_id = _.escapeHTML(notif_data['message_id']); + + this.body = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '
'); + this.cta = _.escapeHTML(notif_data['cta']) || 'Close'; + this.notif_type = _.escapeHTML(notif_data['type']) || 'takeover'; + this.style = _.escapeHTML(notif_data['style']) || 'light'; + this.title = _.escapeHTML(notif_data['title']) || ''; + this.video_width = MPNotif.VIDEO_WIDTH; + this.video_height = MPNotif.VIDEO_HEIGHT; + + // These fields are url-sanitized in the backend already. + this.dest_url = notif_data['cta_url'] || null; + this.image_url = notif_data['image_url'] || null; + this.thumb_image_url = notif_data['thumb_image_url'] || null; + this.video_url = notif_data['video_url'] || null; + + this.clickthrough = true; + if (!this.dest_url) { + this.dest_url = '#dismiss'; + this.clickthrough = false; + } + + this.mini = this.notif_type === 'mini'; + if (!this.mini) { + this.notif_type = 'takeover'; + } + this.notif_width = !this.mini ? MPNotif.NOTIF_WIDTH : MPNotif.NOTIF_WIDTH_MINI; + + this._set_client_config(); + this.imgs_to_preload = this._init_image_html(); + this._init_video(); + }; + + MPNotif = MixpanelLib._Notification; + + MPNotif.ANIM_TIME = 200; + MPNotif.MARKUP_PREFIX = 'mixpanel-notification'; + MPNotif.BG_OPACITY = 0.6; + MPNotif.NOTIF_TOP = 25; + MPNotif.NOTIF_START_TOP = 200; + MPNotif.NOTIF_WIDTH = 388; + MPNotif.NOTIF_WIDTH_MINI = 420; + MPNotif.NOTIF_HEIGHT_MINI = 85; + MPNotif.THUMB_BORDER_SIZE = 5; + MPNotif.THUMB_IMG_SIZE = 60; + MPNotif.THUMB_OFFSET = Math.round(MPNotif.THUMB_IMG_SIZE / 2); + MPNotif.VIDEO_WIDTH = 595; + MPNotif.VIDEO_HEIGHT = 334; + + MPNotif.prototype.show = function() { + var self = this; + this._set_client_config(); + + // don't display until HTML body exists + if (!this.body_el) { + setTimeout(function() { self.show(); }, 300); + return; + } + + this._init_styles(); + this._init_notification_el(); + + // wait for any images to load before showing notification + this._preload_images(this._attach_and_animate); + }; + + MPNotif.prototype.dismiss = _.safewrap(function() { + if (!this.marked_as_shown) { + // unexpected condition: user interacted with notif even though we didn't consider it + // visible (see _mark_as_shown()); send tracking signals to mark delivery + this._mark_delivery({'invisible': true}); + } + + var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el(); + if (this.use_transitions) { + this._remove_class('bg', 'visible'); + this._add_class(exiting_el, 'exiting'); + setTimeout(this._remove_notification_el, MPNotif.ANIM_TIME); + } else { + var notif_attr, notif_start, notif_goal; + if (this.mini) { + notif_attr = 'right'; + notif_start = 20; + notif_goal = -100; + } else { + notif_attr = 'top'; + notif_start = MPNotif.NOTIF_TOP; + notif_goal = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; + } + this._animate_els([ + { + el: this._get_el('bg'), + attr: 'opacity', + start: MPNotif.BG_OPACITY, + goal: 0.0 + }, + { + el: exiting_el, + attr: 'opacity', + start: 1.0, + goal: 0.0 + }, + { + el: exiting_el, + attr: notif_attr, + start: notif_start, + goal: notif_goal + } + ], MPNotif.ANIM_TIME, this._remove_notification_el); + } + }); + + MPNotif.prototype._add_class = _.safewrap(function(el, class_name) { + class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; + if (typeof el === 'string') { + el = this._get_el(el); + } + if (!el.className) { + el.className = class_name; + } else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) { + el.className += ' ' + class_name; + } + }); + MPNotif.prototype._remove_class = _.safewrap(function(el, class_name) { + class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; + if (typeof el === 'string') { + el = this._get_el(el); + } + if (el.className) { + el.className = (' ' + el.className + ' ') + .replace(' ' + class_name + ' ', '') + .replace(/^[\s\xA0]+/, '') + .replace(/[\s\xA0]+$/, ''); + } + }); + + MPNotif.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) { + var self = this, + in_progress = false, + ai, anim, + cur_time = 1 * new Date(), time_diff; + + start_time = start_time || cur_time; + time_diff = cur_time - start_time; + + for (ai = 0; ai < anims.length; ai++) { + anim = anims[ai]; + if (typeof anim.val === 'undefined') { + anim.val = anim.start; + } + if (anim.val !== anim.goal) { + in_progress = true; + var anim_diff = anim.goal - anim.start, + anim_dir = anim.goal >= anim.start ? 1 : -1; + anim.val = anim.start + anim_diff * time_diff / mss; + if (anim.attr !== 'opacity') { + anim.val = Math.round(anim.val); + } + if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) { + anim.val = anim.goal; + } + } + } + if (!in_progress) { + if (done_cb) { + done_cb(); + } + return; + } + + for (ai = 0; ai < anims.length; ai++) { + anim = anims[ai]; + if (anim.el) { + var suffix = anim.attr === 'opacity' ? '' : 'px'; + anim.el.style[anim.attr] = String(anim.val) + suffix; + } + } + setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10); + }); + + MPNotif.prototype._attach_and_animate = _.safewrap(function() { + var self = this; + + // no possibility to double-display + if (this.shown || this._get_shown_campaigns()[this.campaign_id]) { + return; + } + this.shown = true; + + this.body_el.appendChild(this.notification_el); + setTimeout(function() { + var notif_el = self._get_notification_display_el(); + if (self.use_transitions) { + if (!self.mini) { + self._add_class('bg', 'visible'); + } + self._add_class(notif_el, 'visible'); + self._mark_as_shown(); + } else { + var notif_attr, notif_start, notif_goal; + if (self.mini) { + notif_attr = 'right'; + notif_start = -100; + notif_goal = 20; + } else { + notif_attr = 'top'; + notif_start = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; + notif_goal = MPNotif.NOTIF_TOP; + } + self._animate_els([ + { + el: self._get_el('bg'), + attr: 'opacity', + start: 0.0, + goal: MPNotif.BG_OPACITY + }, + { + el: notif_el, + attr: 'opacity', + start: 0.0, + goal: 1.0 + }, + { + el: notif_el, + attr: notif_attr, + start: notif_start, + goal: notif_goal + } + ], MPNotif.ANIM_TIME, self._mark_as_shown); + } + }, 100); + _.register_event(self._get_el('cancel'), 'click', function(e) { + e.preventDefault(); + self.dismiss(); + }); + var click_el = self._get_el('button') || + self._get_el('mini-content'); + _.register_event(click_el, 'click', function(e) { + e.preventDefault(); + if (self.show_video) { + self._track_event('$campaign_open', {'$resource_type': 'video'}); + self._switch_to_video(); + } else { + self.dismiss(); + if (self.clickthrough) { + self._track_event('$campaign_open', {'$resource_type': 'link'}, function() { + win.location.href = self.dest_url; + }); + } + } + }); + }); + + MPNotif.prototype._get_el = function(id) { + return document$1.getElementById(MPNotif.MARKUP_PREFIX + '-' + id); + }; + + MPNotif.prototype._get_notification_display_el = function() { + return this._get_el(this.notif_type); + }; + + MPNotif.prototype._get_shown_campaigns = function() { + return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {}); + }; + + MPNotif.prototype._browser_lte = function(browser, version) { + return this.browser_versions[browser] && this.browser_versions[browser] <= version; + }; + + MPNotif.prototype._init_image_html = function() { + var imgs_to_preload = []; + + if (!this.mini) { + if (this.image_url) { + imgs_to_preload.push(this.image_url); + this.img_html = ''; + } else { + this.img_html = ''; + } + if (this.thumb_image_url) { + imgs_to_preload.push(this.thumb_image_url); + this.thumb_img_html = + '
' + + '' + + '
'; + } else { + this.thumb_img_html = ''; + } + } else { + this.thumb_image_url = this.thumb_image_url || '//cdn.mxpnl.com/site_media/images/icons/notifications/mini-news-dark.png'; + imgs_to_preload.push(this.thumb_image_url); + } + + return imgs_to_preload; + }; + + MPNotif.prototype._init_notification_el = function() { + var notification_html = ''; + var video_src = ''; + var video_html = ''; + var cancel_html = '
' + + '
' + + '
'; + + this.notification_el = document$1.createElement('div'); + this.notification_el.id = MPNotif.MARKUP_PREFIX + '-wrapper'; + if (!this.mini) { + // TAKEOVER notification + var close_html = (this.clickthrough || this.show_video) ? '' : '
', + play_html = this.show_video ? '
' : ''; + if (this._browser_lte('ie', 7)) { + close_html = ''; + play_html = ''; + } + notification_html = + '
' + + this.thumb_img_html + + '
' + + cancel_html + + '
' + + this.img_html + + '
' + this.title + '
' + + '
' + this.body + '
' + + '
' + + 'POWERED BY MIXPANEL' + + '
' + + '
' + + '
' + + close_html + + '' + this.cta + '' + + play_html + + '
' + + '
' + + '
'; + } else { + // MINI notification + notification_html = + '
' + + '
' + + cancel_html + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + this.body + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + } + if (this.youtube_video) { + video_src = '//www.youtube.com/embed/' + this.youtube_video + + '?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080'; + if (this.yt_custom) { + video_src += '&enablejsapi=1&html5=1&controls=0'; + video_html = + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + } + } else if (this.vimeo_video) { + video_src = '//player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0'; + } + if (this.show_video) { + this.video_iframe = + ''; + video_html = + '
' + + '
' + + '
' + + video_html + + '
' + + '
'; + } + var main_html = video_html + notification_html; + if (this.flip_animate) { + main_html = + (this.mini ? notification_html : '') + + '
' + + (this.mini ? video_html : main_html) + + '
'; + } + + this.notification_el.innerHTML = + ('
' + + '
' + + '
' + + '
' + + main_html + + '
' + + '
' + + '
') + .replace(/class=\"/g, 'class="' + MPNotif.MARKUP_PREFIX + '-') + .replace(/id=\"/g, 'id="' + MPNotif.MARKUP_PREFIX + '-'); + }; + + MPNotif.prototype._init_styles = function() { + if (this.style === 'dark') { + this.style_vals = { + bg: '#1d1f25', + bg_actions: '#282b32', + bg_hover: '#3a4147', + bg_light: '#4a5157', + border_gray: '#32353c', + cancel_opacity: '0.4', + mini_hover: '#2a3137', + text_title: '#fff', + text_main: '#9498a3', + text_tagline: '#464851', + text_hover: '#ddd' + }; + } else { + this.style_vals = { + bg: '#fff', + bg_actions: '#e7eaee', + bg_hover: '#eceff3', + bg_light: '#f5f5f5', + border_gray: '#e4ecf2', + cancel_opacity: '1.0', + mini_hover: '#fafafa', + text_title: '#5c6578', + text_main: '#8b949b', + text_tagline: '#ced9e6', + text_hover: '#7c8598' + }; + } + var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)', + video_shadow = shadow, + mini_shadow = shadow, + thumb_total_size = MPNotif.THUMB_IMG_SIZE + MPNotif.THUMB_BORDER_SIZE * 2, + anim_seconds = (MPNotif.ANIM_TIME / 1000) + 's'; + if (this.mini) { + shadow = 'none'; + } + + // don't display on small viewports + var notif_media_queries = {}, + min_width = MPNotif.NOTIF_WIDTH_MINI + 20; + notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = { + '#overlay': { + 'display': 'none' + } + }; + var notif_styles = { + '.flipped': { + 'transform': 'rotateY(180deg)' + }, + '#overlay': { + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'overflow': 'auto', + 'text-align': 'center', + 'z-index': '10000', + 'font-family': '"Helvetica", "Arial", sans-serif', + '-webkit-font-smoothing': 'antialiased', + '-moz-osx-font-smoothing': 'grayscale' + }, + '#overlay.mini': { + 'height': '0', + 'overflow': 'visible' + }, + '#overlay a': { + 'width': 'initial', + 'padding': '0', + 'text-decoration': 'none', + 'text-transform': 'none', + 'color': 'inherit' + }, + '#bgwrapper': { + 'position': 'relative', + 'width': '100%', + 'height': '100%' + }, + '#bg': { + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'min-width': this.doc_width * 4 + 'px', + 'min-height': this.doc_height * 4 + 'px', + 'background-color': 'black', + 'opacity': '0.0', + '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8 + 'filter': 'alpha(opacity=60)', // IE5-7 + 'transition': 'opacity ' + anim_seconds + }, + '#bg.visible': { + 'opacity': MPNotif.BG_OPACITY + }, + '.mini #bg': { + 'width': '0', + 'height': '0', + 'min-width': '0' + }, + '#flipcontainer': { + 'perspective': '1000px', + 'position': 'absolute', + 'width': '100%' + }, + '#flipper': { + 'position': 'relative', + 'transform-style': 'preserve-3d', + 'transition': '0.3s' + }, + '#takeover': { + 'position': 'absolute', + 'left': '50%', + 'width': MPNotif.NOTIF_WIDTH + 'px', + 'margin-left': Math.round(-MPNotif.NOTIF_WIDTH / 2) + 'px', + 'backface-visibility': 'hidden', + 'transform': 'rotateY(0deg)', + 'opacity': '0.0', + 'top': MPNotif.NOTIF_START_TOP + 'px', + 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds + }, + '#takeover.visible': { + 'opacity': '1.0', + 'top': MPNotif.NOTIF_TOP + 'px' + }, + '#takeover.exiting': { + 'opacity': '0.0', + 'top': MPNotif.NOTIF_START_TOP + 'px' + }, + '#thumbspacer': { + 'height': MPNotif.THUMB_OFFSET + 'px' + }, + '#thumbborder-wrapper': { + 'position': 'absolute', + 'top': (-MPNotif.THUMB_BORDER_SIZE) + 'px', + 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET - MPNotif.THUMB_BORDER_SIZE) + 'px', + 'width': thumb_total_size + 'px', + 'height': (thumb_total_size / 2) + 'px', + 'overflow': 'hidden' + }, + '#thumbborder': { + 'position': 'absolute', + 'width': thumb_total_size + 'px', + 'height': thumb_total_size + 'px', + 'border-radius': thumb_total_size + 'px', + 'background-color': this.style_vals.bg_actions, + 'opacity': '0.5' + }, + '#thumbnail': { + 'position': 'absolute', + 'top': '0px', + 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET) + 'px', + 'width': MPNotif.THUMB_IMG_SIZE + 'px', + 'height': MPNotif.THUMB_IMG_SIZE + 'px', + 'overflow': 'hidden', + 'z-index': '100', + 'border-radius': MPNotif.THUMB_IMG_SIZE + 'px' + }, + '#mini': { + 'position': 'absolute', + 'right': '20px', + 'top': MPNotif.NOTIF_TOP + 'px', + 'width': this.notif_width + 'px', + 'height': MPNotif.NOTIF_HEIGHT_MINI * 2 + 'px', + 'margin-top': 20 - MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'backface-visibility': 'hidden', + 'opacity': '0.0', + 'transform': 'rotateX(90deg)', + 'transition': 'opacity 0.3s, transform 0.3s, right 0.3s' + }, + '#mini.visible': { + 'opacity': '1.0', + 'transform': 'rotateX(0deg)' + }, + '#mini.exiting': { + 'opacity': '0.0', + 'right': '-150px' + }, + '#mainbox': { + 'border-radius': '4px', + 'box-shadow': shadow, + 'text-align': 'center', + 'background-color': this.style_vals.bg, + 'font-size': '14px', + 'color': this.style_vals.text_main + }, + '#mini #mainbox': { + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '3px', + 'transition': 'background-color ' + anim_seconds + }, + '#mini-border': { + 'height': (MPNotif.NOTIF_HEIGHT_MINI + 6) + 'px', + 'width': (MPNotif.NOTIF_WIDTH_MINI + 6) + 'px', + 'position': 'absolute', + 'top': '-3px', + 'left': '-3px', + 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '6px', + 'opacity': '0.25', + 'background-color': '#fff', + 'z-index': '-1', + 'box-shadow': mini_shadow + }, + '#mini-icon': { + 'position': 'relative', + 'display': 'inline-block', + 'width': '75px', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '3px 0 0 3px', + 'background-color': this.style_vals.bg_actions, + 'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)', + 'transition': 'background-color ' + anim_seconds + }, + '#mini:hover #mini-icon': { + 'background-color': this.style_vals.mini_hover + }, + '#mini:hover #mainbox': { + 'background-color': this.style_vals.mini_hover + }, + '#mini-icon-img': { + 'position': 'absolute', + 'background-image': 'url(' + this.thumb_image_url + ')', + 'width': '48px', + 'height': '48px', + 'top': '20px', + 'left': '12px' + }, + '#content': { + 'padding': '30px 20px 0px 20px' + }, + '#mini-content': { + 'text-align': 'left', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'cursor': 'pointer' + }, + '#img': { + 'width': '328px', + 'margin-top': '30px', + 'border-radius': '5px' + }, + '#title': { + 'max-height': '600px', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'padding': '25px 0px 20px 0px', + 'font-size': '19px', + 'font-weight': 'bold', + 'color': this.style_vals.text_title + }, + '#body': { + 'max-height': '600px', + 'margin-bottom': '25px', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'line-height': '21px', + 'font-size': '15px', + 'font-weight': 'normal', + 'text-align': 'left' + }, + '#mini #body': { + 'display': 'inline-block', + 'max-width': '250px', + 'margin': '0 0 0 30px', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'font-size': '16px', + 'letter-spacing': '0.8px', + 'color': this.style_vals.text_title + }, + '#mini #body-text': { + 'display': 'table', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px' + }, + '#mini #body-text div': { + 'display': 'table-cell', + 'vertical-align': 'middle' + }, + '#tagline': { + 'margin-bottom': '15px', + 'font-size': '10px', + 'font-weight': '600', + 'letter-spacing': '0.8px', + 'color': '#ccd7e0', + 'text-align': 'left' + }, + '#tagline a': { + 'color': this.style_vals.text_tagline, + 'transition': 'color ' + anim_seconds + }, + '#tagline a:hover': { + 'color': this.style_vals.text_hover + }, + '#cancel': { + 'position': 'absolute', + 'right': '0', + 'width': '8px', + 'height': '8px', + 'padding': '10px', + 'border-radius': '20px', + 'margin': '12px 12px 0 0', + 'box-sizing': 'content-box', + 'cursor': 'pointer', + 'transition': 'background-color ' + anim_seconds + }, + '#mini #cancel': { + 'margin': '7px 7px 0 0' + }, + '#cancel-icon': { + 'width': '8px', + 'height': '8px', + 'overflow': 'hidden', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)', + 'opacity': this.style_vals.cancel_opacity + }, + '#cancel:hover': { + 'background-color': this.style_vals.bg_hover + }, + '#button': { + 'display': 'block', + 'height': '60px', + 'line-height': '60px', + 'text-align': 'center', + 'background-color': this.style_vals.bg_actions, + 'border-radius': '0 0 4px 4px', + 'overflow': 'hidden', + 'cursor': 'pointer', + 'transition': 'background-color ' + anim_seconds + }, + '#button-close': { + 'display': 'inline-block', + 'width': '9px', + 'height': '60px', + 'margin-right': '8px', + 'vertical-align': 'top', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-' + this.style + '.png)', + 'background-repeat': 'no-repeat', + 'background-position': '0px 25px' + }, + '#button-play': { + 'display': 'inline-block', + 'width': '30px', + 'height': '60px', + 'margin-left': '15px', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-' + this.style + '-small.png)', + 'background-repeat': 'no-repeat', + 'background-position': '0px 15px' + }, + 'a#button-link': { + 'display': 'inline-block', + 'vertical-align': 'top', + 'text-align': 'center', + 'font-size': '17px', + 'font-weight': 'bold', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'color': this.style_vals.text_title, + 'transition': 'color ' + anim_seconds + }, + '#button:hover': { + 'background-color': this.style_vals.bg_hover, + 'color': this.style_vals.text_hover + }, + '#button:hover a': { + 'color': this.style_vals.text_hover + }, + + '#video-noflip': { + 'position': 'relative', + 'top': (-this.video_height * 2) + 'px' + }, + '#video-flip': { + 'backface-visibility': 'hidden', + 'transform': 'rotateY(180deg)' + }, + '#video': { + 'position': 'absolute', + 'width': (this.video_width - 1) + 'px', + 'height': this.video_height + 'px', + 'top': MPNotif.NOTIF_TOP + 'px', + 'margin-top': '100px', + 'left': '50%', + 'margin-left': Math.round(-this.video_width / 2) + 'px', + 'overflow': 'hidden', + 'border-radius': '5px', + 'box-shadow': video_shadow, + 'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform + 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds + }, + '#video.exiting': { + 'opacity': '0.0', + 'top': this.video_height + 'px' + }, + '#video-holder': { + 'position': 'absolute', + 'width': (this.video_width - 1) + 'px', + 'height': this.video_height + 'px', + 'overflow': 'hidden', + 'border-radius': '5px' + }, + '#video-frame': { + 'margin-left': '-1px', + 'width': this.video_width + 'px' + }, + '#video-controls': { + 'opacity': '0', + 'transition': 'opacity 0.5s' + }, + '#video:hover #video-controls': { + 'opacity': '1.0' + }, + '#video .video-progress-el': { + 'position': 'absolute', + 'bottom': '0', + 'height': '25px', + 'border-radius': '0 0 0 5px' + }, + '#video-progress': { + 'width': '90%' + }, + '#video-progress-total': { + 'width': '100%', + 'background-color': this.style_vals.bg, + 'opacity': '0.7' + }, + '#video-elapsed': { + 'width': '0', + 'background-color': '#6cb6f5', + 'opacity': '0.9' + }, + '#video #video-time': { + 'width': '10%', + 'right': '0', + 'font-size': '11px', + 'line-height': '25px', + 'color': this.style_vals.text_main, + 'background-color': '#666', + 'border-radius': '0 0 5px 0' + } + }; + + // IE hacks + if (this._browser_lte('ie', 8)) { + _.extend(notif_styles, { + '* html #overlay': { + 'position': 'absolute' + }, + '* html #bg': { + 'position': 'absolute' + }, + 'html, body': { + 'height': '100%' + } + }); + } + if (this._browser_lte('ie', 7)) { + _.extend(notif_styles, { + '#mini #body': { + 'display': 'inline', + 'zoom': '1', + 'border': '1px solid ' + this.style_vals.bg_hover + }, + '#mini #body-text': { + 'padding': '20px' + }, + '#mini #mini-icon': { + 'display': 'none' + } + }); + } + + // add vendor-prefixed style rules + var VENDOR_STYLES = ['backface-visibility', 'border-radius', 'box-shadow', 'opacity', + 'perspective', 'transform', 'transform-style', 'transition'], + VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit']; + for (var selector in notif_styles) { + for (var si = 0; si < VENDOR_STYLES.length; si++) { + var prop = VENDOR_STYLES[si]; + if (prop in notif_styles[selector]) { + var val = notif_styles[selector][prop]; + for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) { + notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val; + } + } + } + } + + var inject_styles = function(styles, media_queries) { + var create_style_text = function(style_defs) { + var st = ''; + for (var selector in style_defs) { + var mp_selector = selector + .replace(/#/g, '#' + MPNotif.MARKUP_PREFIX + '-') + .replace(/\./g, '.' + MPNotif.MARKUP_PREFIX + '-'); + st += '\n' + mp_selector + ' {'; + var props = style_defs[selector]; + for (var k in props) { + st += k + ':' + props[k] + ';'; + } + st += '}'; + } + return st; + }; + var create_media_query_text = function(mq_defs) { + var mqt = ''; + for (var mq in mq_defs) { + mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}'; + } + return mqt; + }; + + var style_text = create_style_text(styles) + create_media_query_text(media_queries), + head_el = document$1.head || document$1.getElementsByTagName('head')[0] || document$1.documentElement, + style_el = document$1.createElement('style'); + head_el.appendChild(style_el); + style_el.setAttribute('type', 'text/css'); + if (style_el.styleSheet) { // IE + style_el.styleSheet.cssText = style_text; + } else { + style_el.textContent = style_text; + } + }; + inject_styles(notif_styles, notif_media_queries); + }; + + MPNotif.prototype._init_video = _.safewrap(function() { + if (!this.video_url) { + return; + } + var self = this; + + // Youtube iframe API compatibility + self.yt_custom = 'postMessage' in win; + + self.dest_url = self.video_url; + var youtube_match = self.video_url.match( + // http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match + /(?:youtube(?:-nocookie)?\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i + ), + vimeo_match = self.video_url.match( + /vimeo\.com\/.*?(\d+)/i + ); + if (youtube_match) { + self.show_video = true; + self.youtube_video = youtube_match[1]; + + if (self.yt_custom) { + win['onYouTubeIframeAPIReady'] = function() { + if (self._get_el('video-frame')) { + self._yt_video_ready(); + } + }; + + // load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference + var tag = document$1.createElement('script'); + tag.src = '//www.youtube.com/iframe_api'; + var firstScriptTag = document$1.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + } + } else if (vimeo_match) { + self.show_video = true; + self.vimeo_video = vimeo_match[1]; + } + + // IE <= 7, FF <= 3: fall through to video link rather than embedded player + if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) { + self.show_video = false; + self.clickthrough = true; + } + }); + + MPNotif.prototype._mark_as_shown = _.safewrap(function() { + // click on background to dismiss + var self = this; + _.register_event(self._get_el('bg'), 'click', function() { + self.dismiss(); + }); + + var get_style = function(el, style_name) { + var styles = {}; + if (document$1.defaultView && document$1.defaultView.getComputedStyle) { + styles = document$1.defaultView.getComputedStyle(el, null); // FF3 requires both args + } else if (el.currentStyle) { // IE + styles = el.currentStyle; + } + return styles[style_name]; + }; + + if (this.campaign_id) { + var notif_el = this._get_el('overlay'); + if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') { + this._mark_delivery(); + } + } + }); + + MPNotif.prototype._mark_delivery = _.safewrap(function(extra_props) { + if (!this.marked_as_shown) { + this.marked_as_shown = true; + + if (this.campaign_id) { + // mark notification shown (local cache) + this._get_shown_campaigns()[this.campaign_id] = 1 * new Date(); + this.persistence.save(); + } + + // track delivery + this._track_event('$campaign_delivery', extra_props); + + // mark notification shown (mixpanel property) + this.mixpanel['people']['append']({ + '$campaigns': this.campaign_id, + '$notifications': { + 'campaign_id': this.campaign_id, + 'message_id': this.message_id, + 'type': 'web', + 'time': new Date() + } + }); + } + }); + + MPNotif.prototype._preload_images = function(all_loaded_cb) { + var self = this; + if (this.imgs_to_preload.length === 0) { + all_loaded_cb(); + return; + } + + var preloaded_imgs = 0; + var img_objs = []; + var onload = function() { + preloaded_imgs++; + if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) { + all_loaded_cb(); + all_loaded_cb = null; + } + }; + for (var i = 0; i < this.imgs_to_preload.length; i++) { + var img = new Image(); + img.onload = onload; + img.src = this.imgs_to_preload[i]; + if (img.complete) { + onload(); + } + img_objs.push(img); + } + + // IE6/7 doesn't fire onload reliably + if (this._browser_lte('ie', 7)) { + setTimeout(function() { + var imgs_loaded = true; + for (i = 0; i < img_objs.length; i++) { + if (!img_objs[i].complete) { + imgs_loaded = false; + } + } + if (imgs_loaded && all_loaded_cb) { + all_loaded_cb(); + all_loaded_cb = null; + } + }, 500); + } + }; + + MPNotif.prototype._remove_notification_el = _.safewrap(function() { + win.clearInterval(this._video_progress_checker); + this.notification_el.style.visibility = 'hidden'; + this.body_el.removeChild(this.notification_el); + }); + + MPNotif.prototype._set_client_config = function() { + var get_browser_version = function(browser_ex) { + var match = navigator.userAgent.match(browser_ex); + return match && match[1]; + }; + this.browser_versions = {}; + this.browser_versions['chrome'] = get_browser_version(/Chrome\/(\d+)/); + this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/); + this.browser_versions['ie'] = get_browser_version(/MSIE (\d+).+/); + if (!this.browser_versions['ie'] && !(win.ActiveXObject) && 'ActiveXObject' in win) { + this.browser_versions['ie'] = 11; + } + + this.body_el = document$1.body || document$1.getElementsByTagName('body')[0]; + if (this.body_el) { + this.doc_width = Math.max( + this.body_el.scrollWidth, document$1.documentElement.scrollWidth, + this.body_el.offsetWidth, document$1.documentElement.offsetWidth, + this.body_el.clientWidth, document$1.documentElement.clientWidth + ); + this.doc_height = Math.max( + this.body_el.scrollHeight, document$1.documentElement.scrollHeight, + this.body_el.offsetHeight, document$1.documentElement.offsetHeight, + this.body_el.clientHeight, document$1.documentElement.clientHeight + ); + } + + // detect CSS compatibility + var ie_ver = this.browser_versions['ie']; + var sample_styles = document$1.createElement('div').style, + is_css_compatible = function(rule) { + if (rule in sample_styles) { + return true; + } + if (!ie_ver) { + rule = rule[0].toUpperCase() + rule.slice(1); + var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule]; + for (var i = 0; i < props.length; i++) { + if (props[i] in sample_styles) { + return true; + } + } + } + return false; + }; + this.use_transitions = this.body_el && + is_css_compatible('transition') && + is_css_compatible('transform'); + this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) && + this.body_el && + is_css_compatible('backfaceVisibility') && + is_css_compatible('perspective') && + is_css_compatible('transform'); + }; + + MPNotif.prototype._switch_to_video = _.safewrap(function() { + var self = this, + anims = [ + { + el: self._get_notification_display_el(), + attr: 'opacity', + start: 1.0, + goal: 0.0 + }, + { + el: self._get_notification_display_el(), + attr: 'top', + start: MPNotif.NOTIF_TOP, + goal: -500 + }, + { + el: self._get_el('video-noflip'), + attr: 'opacity', + start: 0.0, + goal: 1.0 + }, + { + el: self._get_el('video-noflip'), + attr: 'top', + start: -self.video_height * 2, + goal: 0 + } + ]; + + if (self.mini) { + var bg = self._get_el('bg'), + overlay = self._get_el('overlay'); + bg.style.width = '100%'; + bg.style.height = '100%'; + overlay.style.width = '100%'; + + self._add_class(self._get_notification_display_el(), 'exiting'); + self._add_class(bg, 'visible'); + + anims.push({ + el: self._get_el('bg'), + attr: 'opacity', + start: 0.0, + goal: MPNotif.BG_OPACITY + }); + } + + var video_el = self._get_el('video-holder'); + video_el.innerHTML = self.video_iframe; + + var video_ready = function() { + if (win['YT'] && win['YT']['loaded']) { + self._yt_video_ready(); + } + self.showing_video = true; + self._get_notification_display_el().style.visibility = 'hidden'; + }; + if (self.flip_animate) { + self._add_class('flipper', 'flipped'); + setTimeout(video_ready, MPNotif.ANIM_TIME); + } else { + self._animate_els(anims, MPNotif.ANIM_TIME, video_ready); + } + }); + + MPNotif.prototype._track_event = function(event_name, properties, cb) { + if (this.campaign_id) { + properties = properties || {}; + properties = _.extend(properties, { + 'campaign_id': this.campaign_id, + 'message_id': this.message_id, + 'message_type': 'web_inapp', + 'message_subtype': this.notif_type + }); + this.mixpanel['track'](event_name, properties, cb); + } else if (cb) { + cb.call(); + } + }; + + MPNotif.prototype._yt_video_ready = _.safewrap(function() { + var self = this; + if (self.video_inited) { + return; + } + self.video_inited = true; + + var progress_bar = self._get_el('video-elapsed'), + progress_time = self._get_el('video-time'), + progress_el = self._get_el('video-progress'); + + new win['YT']['Player'](MPNotif.MARKUP_PREFIX + '-video-frame', { + 'events': { + 'onReady': function(event) { + var ytplayer = event['target'], + video_duration = ytplayer['getDuration'](), + pad = function(i) { + return ('00' + i).slice(-2); + }, + update_video_time = function(current_time) { + var secs = Math.round(video_duration - current_time), + mins = Math.floor(secs / 60), + hours = Math.floor(mins / 60); + secs -= mins * 60; + mins -= hours * 60; + progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs); + }; + update_video_time(0); + self._video_progress_checker = win.setInterval(function() { + var current_time = ytplayer['getCurrentTime'](); + progress_bar.style.width = (current_time / video_duration * 100) + '%'; + update_video_time(current_time); + }, 250); + _.register_event(progress_el, 'click', function(e) { + var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left); + ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true); + }); + } + } + }); + }); + + // EXPORTS (for closure compiler) + + // MixpanelLib Exports + MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; + MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; + MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; + MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; + MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; + MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; + MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; + MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; + MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; + MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; + MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; + MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; + MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; + MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; + MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; + MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; + MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; + MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; + MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; + MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications; + MixpanelLib.prototype['_show_notification'] = MixpanelLib.prototype._show_notification; + + // MixpanelPersistence Exports + MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; + MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; + MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; + MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; + MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + // MixpanelPeople Exports + MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; + MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; + MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; + MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; + MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; + MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; + MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; + MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; + MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; + MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + + _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']); + + var instances = {}; + var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; + }; + + var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; + }; + + var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); + }; + + var add_dom_event_counting_handlers = function(instance) { + var name = instance.get_config('name'); + + instance.mp_counts = instance.mp_counts || {}; + instance.mp_counts['$__c'] = parseInt(_.cookie.get('mp_' + name + '__c')) || 0; + + var increment_count = function() { + instance.mp_counts['$__c'] = (instance.mp_counts['$__c'] || 0) + 1; + _.cookie.set('mp_' + name + '__c', instance.mp_counts['$__c'], 1, true); + }; + + var evtCallback = function() { + try { + instance.mp_counts = instance.mp_counts || {}; + increment_count(); + } catch (e) { + console$1.error(e); + } + }; + _.register_event(document$1, 'submit', evtCallback); + _.register_event(document$1, 'change', evtCallback); + var mousedownTarget = null; + _.register_event(document$1, 'mousedown', function(e) { + mousedownTarget = e.target; + }); + _.register_event(document$1, 'mouseup', function(e) { + if (e.target === mousedownTarget) { + evtCallback(e); + } + }); + }; + + function init_from_snippet() { + init_type = INIT_SNIPPET; + mixpanel_master = win[PRIMARY_INSTANCE_NAME]; + + // Initialization + if (_.isUndefined(mixpanel_master)) { + // mixpanel wasn't initialized properly, report error and quit + console$1.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.'); + return; + } + if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) { + // lib has already been loaded at least once; we don't want to override the global object this time so bomb early + console$1.error('Mixpanel library has already been downloaded at least once.'); + return; + } + var snippet_version = mixpanel_master['__SV'] || 0; + if (snippet_version < 1.1) { + // mixpanel wasn't initialized properly, report error and quit + console$1.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.'); + return; + } + + // Load instances of the Mixpanel Library + _.each(mixpanel_master['_i'], function(item) { + if (item && _.isArray(item)) { + instances[item[item.length-1]] = create_mplib.apply(this, item); + } + }); + + override_mp_init_func(); + mixpanel_master['init'](); + + // Fire loaded events after updating the window's mixpanel object + _.each(instances, function(instance) { + instance._loaded(); + }); + + add_dom_loaded_handler(); + } + + init_from_snippet(); + +}()); diff --git a/web/webpack.config.js b/web/webpack.config.js index 663183bd..7b73ecc4 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -32,7 +32,12 @@ module.exports = { { test: /third_party\/.*\.js$/, exclude: /(node_modules|bower_components)/, - loader: 'url-loader' + use: { + loader: 'file-loader', + options: { + name: "[name].[ext]" + } + } }, { test: /\.css$/, From e69ecac44b97ee6374931cb31ad080619114ac43 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 17:24:31 +0300 Subject: [PATCH 091/120] Fix user registration --- apps/user/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/user/models.py b/apps/user/models.py index 058df6b6..988ec5ee 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -110,7 +110,9 @@ def send_user_info_to_mixpanel(sender, instance=None, created=False, **kwargs): @receiver(post_save, sender=User) def auto_create_subscription(sender, instance=None, created=False, **kwargs): - if not hasattr(instance, 'email_subscription'): + try: + es = EmailSubscription.objects.get(email=instance.email) + except EmailSubscription.DoesNotExist: instance.email_subscription = EmailSubscription.objects.create( user=instance, email=instance.email, From 8dd4374ceecca82ece466513bd8e8c07d7031f1a Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 17:29:49 +0300 Subject: [PATCH 092/120] Fix email verify link --- apps/auth/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/auth/views.py b/apps/auth/views.py index 3d4ba19d..bd6e25cb 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -48,8 +48,9 @@ class LearnerRegistrationView(FormView): # fixme: change email text # fixme: async send email + refferer = request.META.get('HTTP_REFERER') token = verification_email_token.make_token(user) - url = self.request.scheme + '://' + self.request.get_host() + str(reverse_lazy('lilcity:verification-email', args=[token])) + url = refferer + str(reverse_lazy('lilcity:verification-email', args=[token])) send_email('Verification Email', email, "notification/email/verification_email.html", url=url) return JsonResponse({"success": True}, status=201) From 6336d07e84887fe7bc8bfed0cbc2c958d090da4e Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 17:33:40 +0300 Subject: [PATCH 093/120] Fix create user after create subscribe with same email --- apps/user/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/user/models.py b/apps/user/models.py index 988ec5ee..8d29759f 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -112,6 +112,9 @@ def send_user_info_to_mixpanel(sender, instance=None, created=False, **kwargs): def auto_create_subscription(sender, instance=None, created=False, **kwargs): try: es = EmailSubscription.objects.get(email=instance.email) + if not es.user: + es.user = instance + es.save() except EmailSubscription.DoesNotExist: instance.email_subscription = EmailSubscription.objects.create( user=instance, From 57e4c90c35d7293ff8ffcd51d04c86f1f2f05f2c Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 17:36:17 +0300 Subject: [PATCH 094/120] Fix typo --- apps/auth/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/auth/views.py b/apps/auth/views.py index bd6e25cb..90c04a3b 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -48,7 +48,7 @@ class LearnerRegistrationView(FormView): # fixme: change email text # fixme: async send email - refferer = request.META.get('HTTP_REFERER') + refferer = self.request.META.get('HTTP_REFERER') token = verification_email_token.make_token(user) url = refferer + str(reverse_lazy('lilcity:verification-email', args=[token])) send_email('Verification Email', email, "notification/email/verification_email.html", url=url) From 7187141f438361a3ab0e0d9d0f0b226896f2ff6b Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 21:50:25 +0300 Subject: [PATCH 095/120] Use cached templates --- project/settings.py | 18 ++++++------------ project/urls.py | 11 ++++++----- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/project/settings.py b/project/settings.py index 4a156847..2934c1de 100644 --- a/project/settings.py +++ b/project/settings.py @@ -85,7 +85,6 @@ TEMPLATES = [ 'DIRS': [ 'project', ], - 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'project.context_processors.config', @@ -94,23 +93,18 @@ TEMPLATES = [ 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'loaders': [ + ('django.template.loaders.cached.Loader', [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ]), + ], }, }, ] WSGI_APPLICATION = 'project.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/2.0/ref/settings/#databases - -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), -# } -# } - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', diff --git a/project/urls.py b/project/urls.py index b8f22484..cfa253e1 100644 --- a/project/urls.py +++ b/project/urls.py @@ -13,10 +13,11 @@ Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings from django.contrib import admin -from django.urls import path, include from django.views.generic import TemplateView -from django.conf import settings +from django.urls import path, include + from apps.course.views import ( CoursesView, likes, coursecomment, @@ -82,8 +83,8 @@ if settings.DEBUG: from django.conf.urls.static import static from django.contrib.staticfiles.urls import staticfiles_urlpatterns + urlpatterns += static(settings.STATIC_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += staticfiles_urlpatterns() + if 'silk' in settings.INSTALLED_APPS: urlpatterns += [path('silk/', include('silk.urls', namespace='silk'))] - - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += staticfiles_urlpatterns() From 990be007494bd1fbe510a5c20c1f58bc71518380 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Tue, 27 Mar 2018 21:54:17 +0300 Subject: [PATCH 096/120] Fix static url --- project/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/urls.py b/project/urls.py index cfa253e1..bdcfc266 100644 --- a/project/urls.py +++ b/project/urls.py @@ -83,7 +83,7 @@ if settings.DEBUG: from django.conf.urls.static import static from django.contrib.staticfiles.urls import staticfiles_urlpatterns - urlpatterns += static(settings.STATIC_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += staticfiles_urlpatterns() if 'silk' in settings.INSTALLED_APPS: From 5a3efea65cbd089a2ba1a15ddd28649fa1f926f0 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Wed, 28 Mar 2018 08:40:30 +0300 Subject: [PATCH 097/120] Test deploy --- apps/content/admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/content/admin.py b/apps/content/admin.py index 555ba173..39d1fb93 100644 --- a/apps/content/admin.py +++ b/apps/content/admin.py @@ -63,7 +63,6 @@ class ContentAdmin(PolymorphicParentModelAdmin): Text, ImageText, Video, - # GalleryAdmin, ) From d9fdf6fc8fece6de9b92d5def5bbff9ec945346f Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Wed, 28 Mar 2018 08:43:05 +0300 Subject: [PATCH 098/120] Add collect static command --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b3c84e2f..d848728c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: restart: always volumes: - .:/lilcity - command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload" + command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload" environment: - DJANGO_SETTINGS_MODULE=project.settings - DATABASE_SERVICE_HOST=db From d53356e53ea9761907e262dad6fdcb6dffd33ba0 Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Wed, 28 Mar 2018 09:00:59 +0300 Subject: [PATCH 099/120] LIL-344. Fix password reset --- apps/auth/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/auth/views.py b/apps/auth/views.py index 90c04a3b..8eb52b18 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -107,7 +107,9 @@ class PasswordResetView(views.PasswordContextMixin, BaseFormView): token_generator = views.default_token_generator def form_valid(self, form): + refferer = self.request.META.get('HTTP_REFERER') opts = { + 'domain_override': refferer, 'use_https': self.request.is_secure(), 'token_generator': self.token_generator, 'from_email': self.from_email, From 68a40c860ae83bd35a89000df432325dabb7094e Mon Sep 17 00:00:00 2001 From: Ivlev Denis Date: Wed, 28 Mar 2018 09:03:12 +0300 Subject: [PATCH 100/120] LIL-344. Fix password reset templates --- apps/auth/templates/auth/password_reset.html | 4 ++-- apps/auth/templates/auth/password_reset.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/auth/templates/auth/password_reset.html b/apps/auth/templates/auth/password_reset.html index 0467d253..c611ee51 100644 --- a/apps/auth/templates/auth/password_reset.html +++ b/apps/auth/templates/auth/password_reset.html @@ -3,8 +3,8 @@ {% block content %}

Для восстановления пароля нажмите кнопку ниже.

- Нажмите для восстановления + Нажмите для восстановления

Или скопируйте ссылку ниже, и вставьте её в адресную строку браузера.

-

{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}

+

{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}

{% endblock content %} diff --git a/apps/auth/templates/auth/password_reset.txt b/apps/auth/templates/auth/password_reset.txt index dfd0f38b..403aa7bd 100644 --- a/apps/auth/templates/auth/password_reset.txt +++ b/apps/auth/templates/auth/password_reset.txt @@ -1,2 +1,2 @@ Восстановление пароля для {{ email }}. Перейдите по ссылке ниже: -{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %} \ No newline at end of file +{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %} From cf32370b6df02e637dc48d15d1eec1664e3fef8c Mon Sep 17 00:00:00 2001 From: Vitaly Baev Date: Thu, 29 Mar 2018 10:26:58 +0300 Subject: [PATCH 101/120] LIL-227 LIL-296 LIL-297 --- web/build/img/sprite.svg | 7 + web/package.json | 3 + web/src/components/CourseRedactor.vue | 116 ++++-- web/src/components/blocks/BlockAdd.vue | 5 +- web/src/components/blocks/BlockText.vue | 7 +- web/src/icons/hamburger.svg | 18 + web/src/js/app.js | 1 + web/src/js/modules/api.js | 13 +- web/src/js/modules/notification.js | 14 + web/src/sass/_common.sass | 472 +++++++++++----------- web/src/sass/components/notification.scss | 33 ++ web/src/sass/generated/_sprite-svg.scss | 5 + 12 files changed, 423 insertions(+), 271 deletions(-) create mode 100644 web/src/icons/hamburger.svg create mode 100644 web/src/js/modules/notification.js create mode 100644 web/src/sass/components/notification.scss diff --git a/web/build/img/sprite.svg b/web/build/img/sprite.svg index 2853f1c8..01c98f63 100644 --- a/web/build/img/sprite.svg +++ b/web/build/img/sprite.svg @@ -1,4 +1,6 @@ + + @@ -24,6 +26,11 @@ + + + + + diff --git a/web/package.json b/web/package.json index ba9de0b4..0c6971bf 100755 --- a/web/package.json +++ b/web/package.json @@ -65,9 +65,12 @@ "owl.carousel": "^2.2.0", "slugify": "^1.2.9", "smooth-scroll": "^12.1.5", + "sortablejs": "^1.7.0", + "uuid": "^3.2.1", "validator": "^9.2.0", "vue": "^2.5.13", "vue-autosize": "^1.0.2", + "vuedraggable": "^2.16.0", "vuejs-datepicker": "^0.9.25", "vuelidate": "^0.6.1" } diff --git a/web/src/components/CourseRedactor.vue b/web/src/components/CourseRedactor.vue index d173f8cf..a9580dbd 100644 --- a/web/src/components/CourseRedactor.vue +++ b/web/src/components/CourseRedactor.vue @@ -139,7 +139,7 @@
-
- - - - +
+ - -
+ + + + +
+ @@ -257,6 +259,11 @@ import $ from 'jquery'; import {required, minValue, numeric } from 'vuelidate/lib/validators' import slugify from 'slugify'; + import Draggable from 'vuedraggable'; + import {showNotification} from "../js/modules/notification"; + import createHistory from "history/createBrowserHistory"; + + const history = createHistory(); export default { name: "course-redactor", @@ -422,9 +429,21 @@ }, editLesson(lessonIndex) { this.currentLesson = this.lessons[lessonIndex]; + if (this.viewSection !== 'lessons-edit') { + history.push("/course/create/lessons/new"); + } this.viewSection = 'lessons-edit'; }, + showCourse() { + if (this.viewSection !== 'course') { + history.push("/course/create"); + } + this.viewSection = 'course' + }, showLessons() { + if (this.viewSection !== 'lessons') { + history.push("/course/create/lessons"); + } this.viewSection = 'lessons'; }, addLesson() { @@ -434,6 +453,9 @@ course_id: this.course.id, content: [], }; + if (this.viewSection !== 'lessons-edit') { + history.push("/course/create/lessons/new"); + } this.viewSection = 'lessons-edit'; window.scrollTo(0, 0); }, @@ -515,6 +537,7 @@ onCoursePublish() { this.showErrors = true; if (this.$v.$invalid) { + showNotification("error", "Заполните все необходимые поля"); return; } const publishButton = $('#course-redactor__publish-button'); @@ -583,6 +606,17 @@ } }, mounted() { + // Listen for changes to the current location. + this.unlisten = history.listen((location, action) => { + if (location.pathname === '/course/create/lessons') { + this.viewSection = 'lessons'; + } else if (location.pathname === '/course/create') { + this.viewSection = 'course'; + } else if (location.pathname === '/course/create/lessons/new') { + this.viewSection = 'lessons-edit'; + } + }); + api.getCategories(this.accessToken) .then((response) => { if (response.data) { @@ -677,6 +711,9 @@ return `https://lil.city/course/${suffix}`; }, }, + beforeDestroy() { + this.unlisten(); + }, watch: { 'course': { handler: function (newValue, oldValue) { @@ -707,6 +744,7 @@ 'block-images': BlockImages, 'block-video': BlockVideo, 'lesson-redactor': LessonRedactor, + 'vue-draggable': Draggable, } } @@ -776,4 +814,18 @@ width: 140px; height: 140px; } - \ No newline at end of file + + .kit__section-remove { + button.sortable__handle { + margin-right: 10px; + cursor: -webkit-grab; + cursor: grab; + + svg.icon-hamburger { + width: 1em; + height: 1em; + } + } + } + + diff --git a/web/src/components/blocks/BlockAdd.vue b/web/src/components/blocks/BlockAdd.vue index 7c06a641..0090a8df 100644 --- a/web/src/components/blocks/BlockAdd.vue +++ b/web/src/components/blocks/BlockAdd.vue @@ -51,6 +51,8 @@ \ No newline at end of file + diff --git a/web/src/icons/hamburger.svg b/web/src/icons/hamburger.svg new file mode 100644 index 00000000..b5866b36 --- /dev/null +++ b/web/src/icons/hamburger.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/web/src/js/app.js b/web/src/js/app.js index 9d0023d9..16651100 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -13,6 +13,7 @@ import "./modules/courses"; import "./modules/comments"; import "./modules/password-show"; import "./modules/profile"; +import "./modules/notification"; import "./modules/mixpanel"; import "../sass/app.sass"; diff --git a/web/src/js/modules/api.js b/web/src/js/modules/api.js index 2cfc5385..ad06b7bf 100644 --- a/web/src/js/modules/api.js +++ b/web/src/js/modules/api.js @@ -81,8 +81,10 @@ export const api = { if (block.type === 'text') { return { 'type': 'text', + 'uuid': block.uuid, 'data': { 'id': block.data.id ? block.data.id : null, + 'uuid': block.uuid, 'position': ++index, 'title': block.data.title, 'txt': block.data.text, @@ -93,6 +95,7 @@ export const api = { 'type': 'image', 'data': { 'id': block.data.id ? block.data.id : null, + 'uuid': block.data.uuid, 'position': ++index, 'title': block.data.title, 'img': block.data.image_id, @@ -103,6 +106,7 @@ export const api = { 'type': 'image-text', 'data': { 'id': block.data.id ? block.data.id : null, + 'uuid': block.data.uuid, 'position': ++index, 'title': block.data.title, 'img': block.data.image_id, @@ -114,6 +118,7 @@ export const api = { 'type': 'images', 'data': { 'id': block.data.id ? block.data.id : null, + 'uuid': block.data.uuid, 'position': ++index, 'title': block.data.title, 'images': block.data.images.map((galleryImage) => { @@ -129,6 +134,7 @@ export const api = { 'type': 'video', 'data': { 'id': block.data.id ? block.data.id : null, + 'uuid': block.data.uuid, 'position': ++index, 'title': block.data.title, 'url': block.data.video_url, @@ -271,6 +277,7 @@ export const api = { 'type': 'text', 'data': { 'id': contentItem.id ? contentItem.id : null, + 'uuid': contentItem.uuid, 'title': contentItem.title, 'text': contentItem.txt, } @@ -280,6 +287,7 @@ export const api = { 'type': 'image', 'data': { 'id': contentItem.id ? contentItem.id : null, + 'uuid': contentItem.uuid, 'title': contentItem.title, 'image_id': contentItem.img.id, 'image_url': contentItem.img.image, @@ -290,6 +298,7 @@ export const api = { 'type': 'image-text', 'data': { 'id': contentItem.id ? contentItem.id : null, + 'uuid': contentItem.uuid, 'title': contentItem.title, 'image_id': contentItem.img.id, 'image_url': contentItem.img.image, @@ -301,6 +310,7 @@ export const api = { 'type': 'images', 'data': { 'id': contentItem.id ? contentItem.id : null, + 'uuid': contentItem.uuid, 'title': contentItem.title, 'images': contentItem.gallery_images.map((galleryImage) => { return { @@ -316,6 +326,7 @@ export const api = { 'type': 'video', 'data': { 'id': contentItem.id ? contentItem.id : null, + 'uuid': contentItem.uuid, 'title': contentItem.title, 'video_url': contentItem.url, } @@ -407,4 +418,4 @@ export const api = { } }); } -}; \ No newline at end of file +}; diff --git a/web/src/js/modules/notification.js b/web/src/js/modules/notification.js new file mode 100644 index 00000000..7e614802 --- /dev/null +++ b/web/src/js/modules/notification.js @@ -0,0 +1,14 @@ +import $ from 'jquery'; +import '../../sass/components/notification.scss'; + +export function showNotification(style, text) { + let htmlNode = document.createElement('div'); + let htmlElement = $(htmlNode).addClass('notification').addClass(`notification--${style}`).text(text).appendTo($('body')); + + setTimeout(() => { + htmlElement.fadeOut(400, () => { + htmlElement.remove(); + }) + }, 3500); +} + diff --git a/web/src/sass/_common.sass b/web/src/sass/_common.sass index e1279091..a6bf777f 100755 --- a/web/src/sass/_common.sass +++ b/web/src/sass/_common.sass @@ -37,14 +37,14 @@ $border: #E6E6E6 $cl: #191919 body - min-width: 360px + min-width: 360px +f font-size: 15px - color: $cl + color: $cl line-height: 1.333 +t font-size: 14px - line-height: 1.57 + line-height: 1.57 &.no-scroll overflow: hidden button, @@ -54,9 +54,9 @@ select +f font-size: 15px button - background: none + background: none a - color: $pink + color: $pink text-decoration: none input, textarea @@ -68,11 +68,11 @@ textarea strong, b +fb - + * &::selection background: $pink - color: white + color: white // box-shadow: 0 0 0 1px red button @@ -81,7 +81,7 @@ button .outer display: flex - min-height: 100vh + min-height: 100vh flex-direction: column .container @@ -96,11 +96,11 @@ button +t padding: 0 15px &_md - max-width: 860px + max-width: 860px &_sm - max-width: 700px + max-width: 700px &_xs - max-width: 540px + max-width: 540px .btn position: relative @@ -114,9 +114,9 @@ button letter-spacing: 2px text-align: center transition: all .2s - z-index: 2 + z-index: 2 +t - line-height: 1.33 + line-height: 1.33 &:hover box-shadow: 0 5px 20px 0 rgba(0,0,0,0.11) &:disabled @@ -136,7 +136,7 @@ button .icon position: relative top: -1px - margin-right: 15px + margin-right: 15px fill: $cl transition: fill .2s &_dark @@ -145,14 +145,14 @@ button &:hover background: #545454 &:active - color: rgba(white,.1) + color: rgba(white,.1) &_light background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) &_stroke &:hover, &:active &:before - opacity: 0 + opacity: 0 &:before content: '' position: absolute @@ -162,12 +162,12 @@ button bottom: 1px background: white border-radius: 2px - transition: opacity .2s + transition: opacity .2s z-index: -2 &_gray - background: $bg + background: $bg &_md - padding: 18px 24px 17px + padding: 18px 24px 17px +m padding: 13px 24px 12px &_lg @@ -181,13 +181,13 @@ button &_icon padding-left: 9px padding-right: 9px - background: transparent + background: transparent border: 1px solid $border font-size: 0 .icon margin: 0 font-size: 13px - fill: $cl + fill: $cl &.loading box-shadow: none color: transparent @@ -203,7 +203,7 @@ button margin: -12px 0 0 -12px border: 3px solid $cl border-left: 3px solid transparent - border-radius: 50% + border-radius: 50% animation: loading .6s infinite linear &_fb background: #3957A7 @@ -250,7 +250,7 @@ a.btn background: url(../img/school-lil-city.svg) no-repeat 0 0 / contain +t width: 124px - height: 40px + height: 40px .search position: relative @@ -267,8 +267,8 @@ a.btn padding: 0 15px background: rgba($cl,.1) border-radius: 20px - color: rgba($cl,.8) - transition: width .4s + color: rgba($cl,.8) + transition: width .4s +m width: 100% &__btn @@ -278,7 +278,7 @@ a.btn bottom: 0 width: 40px .icon - font-size: 17px + font-size: 17px fill: rgba($cl,.5) transform: translate(0,2px) &.open &__input @@ -296,7 +296,7 @@ a.btn &__container display: flex height: 100px - border-bottom: 1px solid $border + border-bottom: 1px solid $border align-items: center +t height: 65px @@ -318,9 +318,9 @@ a.btn bottom: 0 background: white flex-direction: column - z-index: 20 + z-index: 20 &.visible - display: block + display: block &__actions display: flex margin: auto @@ -332,7 +332,7 @@ a.btn left: 0 right: 0 padding: 10px 15px - background: white + background: white &__status width: 100px margin-right: 5px @@ -346,7 +346,7 @@ a.btn +m display: flex height: 70px - border-bottom: 1px solid $border + border-bottom: 1px solid $border align-items: center &__search +m @@ -357,29 +357,29 @@ a.btn margin: auto +m display: block - height: calc(100vh - 71px) + height: calc(100vh - 71px) margin: 0 padding: 15px 0 overflow: auto &__group position: relative &__group:hover &__section - color: $cl + color: $cl +m - color: $pink + color: $pink &__group:hover &__list - opacity: 1 + opacity: 1 visibility: visible &__section display: block height: 100px margin: 0 15px border-bottom: 1px solid transparent - +fb + +fb font-size: 12px color: #8C8C8C line-height: 100px - transition: color .2s + transition: color .2s +t height: 65px font-size: 10px @@ -391,17 +391,17 @@ a.btn padding: 15px 40px 15px 20px border: none font-size: 14px - color: $pink - letter-spacing: 2.5px + color: $pink + letter-spacing: 2.5px line-height: 1.33 &_sub +m - &:before, + &:before, &:after content: '' position: absolute top: 50% - background: $gray + background: $gray transform: translateY(-50%) &:before right: 20px @@ -410,7 +410,7 @@ a.btn &:after right: 25px width: 2px - height: 12px + height: 12px &.active border-color: $cl color: $cl @@ -419,7 +419,7 @@ a.btn &.open +m &:after - opacity: 0 + opacity: 0 &__list, &__drop position: absolute @@ -433,7 +433,7 @@ a.btn border-radius: 2px opacity: 0 visibility: hidden - transform: translate3d(0,0,0) + transform: translate3d(0,0,0) transition: opacity .2s, visibility .2s overflow: hidden &__list @@ -460,7 +460,7 @@ a.btn &__balance margin-bottom: 10px padding: 0 20px 10px - border-bottom: 1px solid $border + border-bottom: 1px solid $border &__link display: block position: relative @@ -479,7 +479,7 @@ a.btn &_green color: $green-light +m - color: $green-light + color: $green-light &:hover color: darken($green-light,10) &_gray @@ -489,7 +489,7 @@ a.btn &_border margin-bottom: 5px padding: 2px 20px 10px - border-bottom: 1px solid $border + border-bottom: 1px solid $border color: $cl &__link.active &__title &:after @@ -529,19 +529,19 @@ a.btn +fb font-size: 12px color: #8C8C8C - line-height: 100px - transition: color .2s + line-height: 100px + transition: color .2s +t height: 65px margin-left: 25px font-size: 10px - line-height: 65px + line-height: 65px +m margin: 0 font-size: 0 - line-height: 1 + line-height: 1 &:hover - color: $cl + color: $cl .icon display: none +m @@ -560,7 +560,7 @@ a.btn padding: 15px 20px &__login:hover &__drop opacity: 1 - visibility: visible + visibility: visible &__ava height: 40px &_bg &__container @@ -573,15 +573,15 @@ a.btn margin-top: -100px padding: 120px 0 50px background-position: 50% 50% - background-repeat: no-repeat - background-size: cover + background-repeat: no-repeat + background-size: cover text-align: center - z-index: 4 + z-index: 4 +t - min-height: 350px - padding: 137px 0 40px + min-height: 350px + padding: 137px 0 40px +m - min-height: 400px + min-height: 400px &:before content: '' position: absolute @@ -590,20 +590,20 @@ a.btn right: 0 bottom: 0 background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) - opacity: .8 + opacity: .8 z-index: -2 &__center margin: auto &__title max-width: 780px - margin: 0 auto 40px + margin: 0 auto 40px +fb font-size: 50px - line-height: (6/5) + line-height: (6/5) +t - max-width: 400px + max-width: 400px font-size: 24px - line-height: 1.33 + line-height: 1.33 +m margin-bottom: 50px &__content @@ -618,12 +618,12 @@ a.btn &:before background-image: linear-gradient(-225deg, #E2E2E2 0%, #E2FFDF 100%) &_sm - min-height: 0 + min-height: 0 padding: 135px 0 55px +t min-height: 0 +m - min-height: 0 + min-height: 0 &_sm &__title margin-bottom: 0 @@ -646,7 +646,7 @@ a[name] text-align: center &_sm margin-bottom: 20px - font-size: 25px + font-size: 25px .subtitle margin-bottom: 25px @@ -671,7 +671,7 @@ a[name] text-align: center z-index: 2 +t - max-width: 400px + max-width: 400px margin-bottom: 40px font-size: 14px line-height: 1.5 @@ -741,7 +741,7 @@ a[name] +m padding: 30px 0 &_review - background: url(../img/bg-elephants.jpg) 0 0 / 100px 102px + background: url(../img/bg-elephants.jpg) 0 0 / 100px 102px &_gray background: $bg &_pink-light @@ -756,10 +756,10 @@ a[name] width: 100% max-width: 944px height: 1px - background: $border + background: $border transform: translateX(-50%) +t - max-width: calc(100% - 30px) + max-width: calc(100% - 30px) &_gradient background-image: linear-gradient(-225deg, #F8F8F8 0%, #FFF2F2 100%) &_tabs @@ -835,7 +835,7 @@ a[name] align-items: center &__ava height: 40px - margin-right: 10px + margin-right: 10px flex: 0 0 40px &__ava height: 40px @@ -863,7 +863,7 @@ a[name] font-size: 15px &_white &__date - color: white + color: white .video display: block @@ -886,9 +886,9 @@ a[name] top: 0 left: 0 width: 100% - height: 100% + height: 100% padding: 20px - background: rgba($cl,.5) + background: rgba($cl,.5) align-items: center justify-content: center flex-direction: column @@ -929,7 +929,7 @@ a[name] +t max-width: calc(100% - 55px) +m - max-width: 100% + max-width: 100% &__more text-align: center +t @@ -940,10 +940,10 @@ a[name] position: relative padding: 55px 0 50px background-position: 50% 50% - background-repeat: no-repeat + background-repeat: no-repeat background-size: cover color: white - text-align: center + text-align: center z-index: 4 +t padding: 30px 0 @@ -962,11 +962,11 @@ a[name] margin-bottom: 85px +fb font-size: 20px - letter-spacing: 4px + letter-spacing: 4px +t margin-bottom: 30px font-size: 12px - letter-spacing: 2px + letter-spacing: 2px +m margin-bottom: 40px &__title @@ -982,7 +982,7 @@ a[name] +t margin-bottom: 65px +m - max-width: 80% + max-width: 80% margin-bottom: 40px &__action +fb @@ -1002,7 +1002,7 @@ a[name] text-align: center flex-wrap: wrap justify-content: center - z-index: 4 + z-index: 4 +t margin: 0 +m @@ -1035,17 +1035,17 @@ a[name] font-size: 37px &__title display: flex - min-height: 38px + min-height: 38px margin-bottom: 10px +fb font-size: 14px letter-spacing: 2.5px - line-height: 1.42 + line-height: 1.42 text-transform: uppercase align-items: center justify-content: center +m - min-height: 0 + min-height: 0 &__content +t font-size: 12px @@ -1064,7 +1064,7 @@ a[name] margin-bottom: 40px &__ava height: 60px - margin-right: 20px + margin-right: 20px flex: 0 0 60px +t height: 45px @@ -1081,7 +1081,7 @@ a[name] &__name +fb font-size: 15px - line-height: 1.33 + line-height: 1.33 .gallery display: flex @@ -1117,11 +1117,11 @@ a[name] margin: 0 10px 20px +m width: calc(33.33% - 10px) - margin: 0 5px 10px + margin: 0 5px 10px &_lg width: calc(66.66% - 20px) +m - width: calc(66.66% - 10px) + width: calc(66.66% - 10px) &:nth-child(4) clear: both &__pic @@ -1145,7 +1145,7 @@ a[name] padding: 50px 0 60px flex: 0 0 49% text-align: center - z-index: 2 + z-index: 2 +t padding: 30px 0 35px flex: 0 0 47% @@ -1202,9 +1202,9 @@ a[name] &__pic display: block max-width: 100% - max-height: 277px + max-height: 277px +m - max-height: 185px + max-height: 185px a.grey-link color: #A7A7A7 @@ -1226,7 +1226,7 @@ a.grey-link &__wrap max-width: 375px +t - max-width: 220px + max-width: 220px +m max-width: 180px &__theme @@ -1244,28 +1244,28 @@ a.grey-link margin-bottom: 30px +fs font-size: 18px - line-height: 1.33 + line-height: 1.33 +m margin-bottom: 15px font-size: 14px &__btn - background: transparent + background: transparent padding: 11px 24px 10px border: 2px solid $cl - color: $cl + color: $cl &:hover - background: $cl + background: $cl border-color: $cl color: white &:active - color: rgba(white,.1) + color: rgba(white,.1) &__preview position: absolute top: 0 left: calc(50% - 145px) bottom: 0 +t - left: calc(50% - 110px) + left: calc(50% - 110px) +m left: 50% &__pic @@ -1300,31 +1300,31 @@ a.grey-link letter-spacing: 4px +t font-size: 12px - letter-spacing: 2px + letter-spacing: 2px &__name margin-bottom: 20px +fb &__content font-size: 16px - line-height: (22/16) + line-height: (22/16) .toggle font-size: 14px &__head - color: $pink + color: $pink .icon position: relative top: -1px margin-left: 10px font-size: 6px - transition: transform .2s + transition: transform .2s &.active .icon transform: rotate(-180deg) &__body display: none padding-top: 10px - color: #888 + color: #888 .schedule margin-bottom: 70px @@ -1379,7 +1379,7 @@ a.grey-link &__item display: block margin: 0 10px 60px - color: $cl + color: $cl flex: 0 0 calc(33.33% - 20px) +t margin-bottom: 40px @@ -1389,7 +1389,7 @@ a.grey-link display: block position: relative margin-bottom: 15px - color: $cl + color: $cl +t margin-bottom: 10px &__preview:hover &__view @@ -1408,13 +1408,13 @@ a.grey-link height: 30px right: 10px &_fav - background-image: url(../img/fav.svg) + background-image: url(../img/fav.svg) &_clock - background-image: url(../img/clock.svg) + background-image: url(../img/clock.svg) &__pic display: block width: 100% - border-radius: 2px + border-radius: 2px &__view display: flex position: absolute @@ -1423,13 +1423,13 @@ a.grey-link width: 100% height: 100% background-image: linear-gradient(-225deg, #FFE2EB 0%, #D8F5F5 100%) - border-radius: 2px + border-radius: 2px opacity: 0 font-size: 18px align-items: center justify-content: center transition: opacity .2s - z-index: 4 + z-index: 4 &__soon display: flex position: absolute @@ -1439,7 +1439,7 @@ a.grey-link bottom: 0 padding: 10px background: rgba(black,.4) - color: white + color: white flex-direction: column justify-content: center text-align: center @@ -1455,7 +1455,7 @@ a.grey-link letter-spacing: 3px +t font-size: 12px - letter-spacing: 1px + letter-spacing: 1px &__details display: flex margin-bottom: 10px @@ -1464,13 +1464,13 @@ a.grey-link +fb font-size: 12px letter-spacing: 2px - color: $cl + color: $cl &__title display: block margin-bottom: 10px +fs font-size: 18px - color: $cl + color: $cl +t line-height: 1.33 &__user @@ -1513,14 +1513,14 @@ a.grey-link margin-top: 30px &__btn position: relative - min-width: 300px + min-width: 300px padding: 19px 24px 18px - border-radius: 28px - letter-spacing: 2px + border-radius: 28px + letter-spacing: 2px z-index: 4 &:hover &:before - opacity: 0 + opacity: 0 &:before content: '' position: absolute @@ -1529,9 +1529,9 @@ a.grey-link right: 5px bottom: 5px background: white - border-radius: 24px - transition: opacity .2s - z-index: -2 + border-radius: 24px + transition: opacity .2s + z-index: -2 .partners display: flex @@ -1555,7 +1555,7 @@ a.grey-link display: block position: relative font-size: 13px - user-select: none + user-select: none cursor: pointer &__input position: absolute @@ -1564,7 +1564,7 @@ a.grey-link opacity: 0 &__input:checked + &__content &:after - opacity: 1 + opacity: 1 &__content display: block padding: 3px 0 3px 30px @@ -1613,18 +1613,18 @@ a.grey-link text-align: right &_blue &__content &:after - background: #4A90E2 + background: #4A90E2 &_lesson &__content display: flex padding: 19px 0 19px 50px border-bottom: 1px solid $border font-size: 16px - color: $gray + color: $gray transition: color .2s +t padding: 10px 0 10px 40px flex-wrap: wrap - line-height: 1.33 + line-height: 1.33 &:before, &:after transform: translateY(18px) @@ -1637,7 +1637,7 @@ a.grey-link &_circle &__content &:before, &:after - border-radius: 50% + border-radius: 50% &_lg &__content padding: 0 0 0 30px font-size: 16px @@ -1647,7 +1647,7 @@ a.grey-link .footer padding: 50px 0 30px - background: $bg + background: $bg +t padding: 30px 0 35px &__row @@ -1719,9 +1719,9 @@ a.grey-link max-width: 200px font-size: 18px +t - max-width: 250px + max-width: 250px font-size: 14px - line-height: 1.33 + line-height: 1.33 &__title padding: 20px 0 50px +fb @@ -1730,12 +1730,12 @@ a.grey-link text-transform: uppercase +t padding: 0 0 20px - letter-spacing: 2px + letter-spacing: 2px &__link, &__contact display: table font-size: 14px - color: $cl + color: $cl line-height: (20/14) +t padding: 5px 0 @@ -1757,7 +1757,7 @@ a.grey-link +t display: none &__links &__link - color: #888 + color: #888 &__divider margin: 0 10px @@ -1785,7 +1785,7 @@ a.grey-link &:not(:last-child) margin-right: 20px .icon - &-instagram + &-instagram font-size: 18px &-twitter font-size: 17px @@ -1796,8 +1796,8 @@ a.grey-link .select position: relative - user-select: none - z-index: 4 + user-select: none + z-index: 4 &__head position: relative height: 36px @@ -1807,7 +1807,7 @@ a.grey-link line-height: 36px white-space: nowrap text-overflow: ellipsis - color: $gray + color: $gray cursor: pointer transition: border-color .2s padding-right: 15px @@ -1817,12 +1817,12 @@ a.grey-link position: absolute top: 14px right: 0 - +arr(8,8,$cl,b) + +arr(8,8,$cl,b) &__drop position: absolute left: 0 right: 0 - top: calc(100% + 10px) + top: calc(100% + 10px) padding: 10px 0 background: white box-shadow: 0 2px 20px 0 rgba(0,0,0,0.10) @@ -1845,7 +1845,7 @@ a.grey-link +m padding: 11px 20px font-size: 12px - color: $cl + color: $cl &:hover color: $cl &__option.active @@ -1864,7 +1864,7 @@ a.grey-link display: table position: relative &.active - z-index: 99 + z-index: 99 &.active &__head border-color: $cl &:after @@ -1873,7 +1873,7 @@ a.grey-link opacity: 1 visibility: visible &.selected &__head - color: $cl + color: $cl .error.info__field--light .select @@ -1905,14 +1905,14 @@ a.grey-link &_gray color: $gray &__link - margin-left: auto + margin-left: auto &__wrap position: relative &__input, &__textarea width: 100% border-bottom: 1px solid $border - background: transparent + background: transparent font-size: 18px transition: border-color .2s &:focus @@ -1930,7 +1930,7 @@ a.grey-link &_bg height: 50px padding: 0 20px - &__textarea + &__textarea display: block height: auto padding: 7px 0 @@ -1963,7 +1963,7 @@ a.grey-link &_info &__input, &_info &__textarea border-color: rgba($border,.2) - color: white + color: white &:focus border-color: white &.error &__input, @@ -1983,7 +1983,7 @@ a.grey-link margin-top: 11px .field__wrap &--title - margin-right: 25px + margin-right: 60px &--additional margin-top: 10px svg.icon-password-eye @@ -2013,14 +2013,14 @@ a.grey-link +fb font-size: 12px letter-spacing: 2px - line-height: 60px - color: $gray + line-height: 60px + color: $gray text-transform: uppercase text-align: center flex: 1 0 50% - transition: border-color .2s, color .2s + transition: border-color .2s, color .2s &.active - border-color: $cl + border-color: $cl color: $cl &__tab display: none @@ -2038,12 +2038,12 @@ a.grey-link font-size: 12px color: $cl text-decoration: underline - line-height: 1.5 + line-height: 1.5 &__foot margin-top: 30px &__btn width: 100% - padding: 18px 24px 17px + padding: 18px 24px 17px &__or padding: 10px 0 font-size: 14px @@ -2063,7 +2063,7 @@ a.grey-link left: 0 right: 0 bottom: 0 - min-width: 360px + min-width: 360px padding: 15px background: rgba($cl,.7) opacity: 0 @@ -2078,12 +2078,12 @@ a.grey-link background: white box-shadow: 0 2px 20px rgba(0,0,0,0.10) border-radius: 3px - transform: scale(.9) + transform: scale(.9) transition: transform .2s &_md - max-width: 620px + max-width: 620px &_lg - max-width: 1000px + max-width: 1000px &__close position: absolute top: -15px @@ -2091,18 +2091,18 @@ a.grey-link padding: 10px background: $cl border-radius: 50% - box-shadow: 0 2px 20px rgba(0,0,0,0.10) + box-shadow: 0 2px 20px rgba(0,0,0,0.10) font-size: 0 .icon font-size: 14px fill: white &.open - display: flex + display: flex &.visible opacity: 1 &.visible &__wrap - transform: scale(1) - + transform: scale(1) + .head display: flex margin-bottom: 50px @@ -2122,7 +2122,7 @@ a.grey-link margin: 0 padding: 0 20px 0 260px flex: 0 0 calc(100% - 220px) - z-index: 4 + z-index: 4 +t margin: 0 0 15px padding: 0 @@ -2133,7 +2133,7 @@ a.grey-link left: calc(50% - 60px) transform: translateY(-50%) z-index: -2 - + .layout display: flex margin: 0 -10px @@ -2171,7 +2171,7 @@ a.grey-link &__pic display: block width: 100% - border-radius: 2px + border-radius: 2px &__wrap flex: 0 0 calc(100% - 95px) &__title @@ -2190,9 +2190,9 @@ a.grey-link margin-bottom: 30px &__item display: flex - max-width: calc(50% - 20px) + max-width: calc(50% - 20px) align-items: center - color: $cl + color: $cl &__arrow margin-right: 15px font-size: 0 @@ -2205,7 +2205,7 @@ a.grey-link font-size: 12px &__title font-size: 18px - line-height: 1.33 + line-height: 1.33 +t font-size: 14px &__title + &__arrow @@ -2247,7 +2247,7 @@ a.grey-link margin-bottom: 60px &__head display: flex - min-height: 40px + min-height: 40px margin-bottom: 30px align-items: center justify-content: space-between @@ -2276,15 +2276,15 @@ a.grey-link background: rgba(black,.3) &__pic display: block - width: 100% + width: 100% &__btn position: absolute top: 50% left: 50% - min-width: 200px - background: transparent + min-width: 200px + background: transparent border: 2px solid white - color: white + color: white transform: translate(-50%,-50%) overflow: hidden &:active @@ -2299,7 +2299,7 @@ a.grey-link +m display: block &__buy - width: 220px + width: 220px margin-left: auto +m width: auto @@ -2313,7 +2313,7 @@ a.grey-link margin-bottom: 25px &__metas &__meta +m - margin-bottom: 20px + margin-bottom: 20px &__actions display: flex margin: 0 -10px 50px @@ -2340,8 +2340,8 @@ a.grey-link padding: 100px 0 110px background-size: cover background-position: 50% 50% - background-repeat: no-repeat - color: white + background-repeat: no-repeat + color: white z-index: 4 +t padding: 60px 0 @@ -2356,7 +2356,7 @@ a.grey-link left: 0 width: 100% height: 100% - background-image: linear-gradient(0deg, rgba(0,0,0,0.24) 0%, rgba(0,0,0,0.64) 100%) + background-image: linear-gradient(0deg, rgba(0,0,0,0.24) 0%, rgba(0,0,0,0.64) 100%) z-index: -2 &:after left: 50% @@ -2367,7 +2367,7 @@ a.grey-link color: white .icon fill: white - + .lessons &__list margin-bottom: 60px @@ -2376,12 +2376,12 @@ a.grey-link &__item position: relative margin-bottom: 40px - transition: box-shadow .2s + transition: box-shadow .2s +m margin-bottom: 30px &__item:hover &__actions opacity: 1 - visibility: visible + visibility: visible &__subtitle margin-bottom: 20px &__row @@ -2389,7 +2389,7 @@ a.grey-link +m display: block &__preview - margin-right: 25px + margin-right: 25px flex: 0 0 140px +m display: none @@ -2405,7 +2405,7 @@ a.grey-link right: 10px opacity: 0 visibility: hidden - transition: opacity .2s, visibility .2s + transition: opacity .2s, visibility .2s &__action margin-left: 10px padding: 10px @@ -2416,7 +2416,7 @@ a.grey-link .icon font-size: 20px fill: #C8C8C8 - transition: fill .2s + transition: fill .2s &__foot text-align: center &__btn @@ -2426,10 +2426,10 @@ a.grey-link padding: 20px &:hover box-shadow: 0 10px 50px 0 rgba(0,0,0,0.06) - border-radius: 10px + border-radius: 10px &_kit &__content flex: 0 0 100% - + .lesson &__subtitle margin-bottom: 10px @@ -2445,7 +2445,7 @@ a.grey-link &__content margin-bottom: 30px color: #191919 - + .materials &__item display: flex @@ -2486,7 +2486,7 @@ a.grey-link &__pic display: block width: 100% - + .questions &__anchor display: block; @@ -2516,7 +2516,7 @@ a.grey-link background: white border-radius: 0 10px 10px 10px +m - padding: 10px + padding: 10px &__ava height: 60px margin-right: 20px @@ -2539,15 +2539,15 @@ a.grey-link width: 100% height: 70px padding: 11px 15px - border-radius: 2px + border-radius: 2px font-size: 16px - resize: vertical + resize: vertical +m height: 64px &__btn display: block margin: 0 auto - border-radius: 20px + border-radius: 20px &__details margin-bottom: 5px &__head, @@ -2562,7 +2562,7 @@ a.grey-link margin-bottom: 5px &__action, &__date - color: $gray + color: $gray &__author margin-right: 15px &__date @@ -2592,7 +2592,7 @@ a.grey-link span:first-child display: none; &_sm &__title - margin-bottom: 15px + margin-bottom: 15px &_sm &__item &:not(:last-child) margin-right: 10px @@ -2628,7 +2628,7 @@ a.grey-link +m flex: 0 0 100% &__head_main - padding-top: 25px + padding-top: 25px +t padding-top: 21px &__label @@ -2659,7 +2659,7 @@ a.grey-link &__wrap padding: 20px 30px 30px background: white - border-radius: 6px + border-radius: 6px +t padding: 15px 20px 20px &__title @@ -2681,7 +2681,7 @@ a.grey-link display: flex margin: 30px -30px -10px padding: 20px 30px 0 - border-top: 1px solid $border + border-top: 1px solid $border +fb font-size: 16px +t @@ -2723,7 +2723,7 @@ a.grey-link padding: 0 &__btn padding: 10px 15px - background: transparent + background: transparent border: 1px solid $gray color: $gray &_edit @@ -2756,12 +2756,12 @@ a.grey-link +fb font-size: 24px letter-spacing: 0 - text-transform: none + text-transform: none &__share margin-bottom: 35px &__content max-width: 760px - margin: 0 auto + margin: 0 auto &__foot display: none margin-top: 30px @@ -2781,7 +2781,7 @@ a.grey-link margin: 0 -15px 30px &__btn height: 56px - border-bottom: 1px solid $border + border-bottom: 1px solid $border +fb font-size: 12px color: $gray @@ -2794,7 +2794,7 @@ a.grey-link +m margin: 0 &:hover - color: $cl + color: $cl &.active border-color: $cl color: $cl @@ -2811,11 +2811,11 @@ a.grey-link margin-right: 30px &.active +fb - color: $cl - + color: $cl + .confirm &__title - margin-bottom: 20px + margin-bottom: 20px font-size: 24px &__form display: flex @@ -2830,7 +2830,7 @@ a.grey-link &__field +m margin-bottom: 20px - + .form position: relative &__ava @@ -2841,7 +2841,7 @@ a.grey-link +t position: relative left: 0 - margin-bottom: 40px + margin-bottom: 40px &__group position: relative margin-bottom: 40px @@ -2923,8 +2923,8 @@ a.grey-link margin-top: 40px &__btn width: 100% - max-width: 300px - + max-width: 300px + .author &__row display: flex @@ -2950,38 +2950,38 @@ a.grey-link font-size: 12px letter-spacing: 2px +m - margin-bottom: 10px + margin-bottom: 10px &__name font-size: 10px letter-spacing: 1.66px &__content - margin-bottom: 30px + margin-bottom: 30px +m margin-bottom: 10px .upload position: relative font-size: 18px - color: $blue + color: $blue overflow: hidden &__file position: absolute top: 0 right: 0 - opacity: 0 + opacity: 0 cursor: pointer .info display: flex - background: $bg + background: $bg +m display: block margin-top: 60px &__section display: flex position: relative - background-position: 50% 50% - background-size: cover + background-position: 50% 50% + background-size: cover flex: 0 0 calc(50% + 169px) justify-content: flex-end z-index: 4 @@ -3005,7 +3005,7 @@ a.grey-link flex-grow: 1 +t width: 100% - max-width: 100% + max-width: 100% padding: 30px 15px 50px &__head display: flex @@ -3030,7 +3030,7 @@ a.grey-link &__upload margin-left: auto font-size: 16px - color: rgba(white,.7) + color: rgba(white,.7) &__foot display: flex margin-top: auto @@ -3041,11 +3041,11 @@ a.grey-link flex: 0 0 140px flex-grow: 1 &__wrap - max-width: 349px + max-width: 349px height: 550px padding: 30px 40px 30px 30px +t - max-width: 100% + max-width: 100% height: auto padding: 30px 15px 30px &__wrap @@ -3073,7 +3073,7 @@ a.grey-link margin-right: 10px padding: 19px border: 1px solid $border - border-radius: 50% + border-radius: 50% .icon font-size: 20px fill: #B5B5B5 @@ -3174,7 +3174,7 @@ a.grey-link top: 0 right: 0 font-size: 100px - opacity: 0 + opacity: 0 cursor: pointer &__fieldset flex: 0 0 calc(100% - 160px) @@ -3206,14 +3206,14 @@ a.grey-link .editor position: relative - + .fontstyle position: absolute top: -40px left: 40px padding: 0 15px background: $cl - border-radius: 35px + border-radius: 35px font-size: 0 white-space: nowrap &__regular, @@ -3222,12 +3222,12 @@ a.grey-link width: 28px height: 35px font-size: 16px - color: white + color: white &__bold +fb &__italic font-style: italic - + .ui-datepicker display: none width: 240px @@ -3235,7 +3235,7 @@ a.grey-link padding: 5px background: white box-shadow: 0 2px 20px 0 rgba(0,0,0,0.10) - z-index: 99!important + z-index: 99!important &-header display: flex margin-bottom: 5px @@ -3251,11 +3251,11 @@ a.grey-link width: 10px height: 10px border: solid $border - border-width: 2px 2px 0 0 + border-width: 2px 2px 0 0 &-prev order: 1 &:before - transform: rotate(-135deg) + transform: rotate(-135deg) &-title text-align: center order: 2 @@ -3278,12 +3278,12 @@ a.grey-link display: block padding: 7px color: #8C8C8C - transition: color .2s + transition: color .2s &:hover - color: $cl + color: $cl &.ui-state-active - background: $bg - color: $pink + background: $bg + color: $pink .form__common-error margin-top: 15px @@ -3304,7 +3304,7 @@ a.grey-link background: $pink &_info background: $green - + .mobile-hide +m @@ -3317,14 +3317,14 @@ a.grey-link height: 40px padding: 0 10px border: 1px solid $border - border-radius: 3px 0 0 3px + border-radius: 3px 0 0 3px font-size: 18px transition: border-color .2s &:focus border-color: $cl flex: 0 0 calc(100% - 100px) &__btn - border-radius: 0 3px 3px 0 + border-radius: 0 3px 3px 0 flex: 0 0 100px a diff --git a/web/src/sass/components/notification.scss b/web/src/sass/components/notification.scss new file mode 100644 index 00000000..38773913 --- /dev/null +++ b/web/src/sass/components/notification.scss @@ -0,0 +1,33 @@ +.notification { + min-width: 380px; + box-sizing: border-box; + border-radius: 4px; + border-width: 1px; + border-style: solid; + border-color: #ebeef5; + position: fixed; + left: 50%; + top: 20px; + transform: translateX(-50%); + background-color: #edf2fc; + transition: opacity 0.3s, transform .4s; + overflow: hidden; + padding: 15px 15px 15px 20px; + display: flex; + align-items: center; + z-index: 1000; + + &--success { + background: #53CF86; + color: #fff; + border: none; + box-shadow: 0 4px 15px rgba(0, 196, 83, 0.31); + } + + &--error { + background: #D12424; + color: #fff; + border: none; + box-shadow: 0 4px 15px rgba(255, 36, 36, 0.51); + } +} diff --git a/web/src/sass/generated/_sprite-svg.scss b/web/src/sass/generated/_sprite-svg.scss index 88817d5d..729565d4 100755 --- a/web/src/sass/generated/_sprite-svg.scss +++ b/web/src/sass/generated/_sprite-svg.scss @@ -53,6 +53,11 @@ height: 1em; fill: #1C2635; } +.icon-hamburger { + width: 1.25em; + height: 1em; + fill: #C8C8C8; +} .icon-image-text { width: 2.07em; height: 1em; From 3944615195e7adfcb064edd8da9a3e47e49dddef Mon Sep 17 00:00:00 2001 From: Sanasol Date: Mon, 2 Apr 2018 12:52:40 +0300 Subject: [PATCH 102/120] fix comments --- api/v1/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/api/v1/views.py b/api/v1/views.py index c47ce413..2782830b 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -341,11 +341,13 @@ class CommentViewSet(ExtendedModelViewSet): queryset = self.queryset is_deactivated = self.request.query_params.get('is_deactivated', '0') if is_deactivated == '0': - return queryset + queryset = queryset elif is_deactivated == '1': - return queryset.filter(deactivated_at__isnull=True) + queryset = queryset.filter(deactivated_at__isnull=True) elif is_deactivated == '2': - return queryset.filter(deactivated_at__isnull=False) + queryset = queryset.filter(deactivated_at__isnull=False) + + return queryset class AuthorRequestViewSet(ExtendedModelViewSet): From 8157102c145a96086ada4879d6c31fe0de21f6d8 Mon Sep 17 00:00:00 2001 From: Sanasol Date: Mon, 2 Apr 2018 13:13:39 +0300 Subject: [PATCH 103/120] fix footer year --- project/templates/lilcity/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index dbdbcfe2..4e3c17e2 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -248,7 +248,7 @@