├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── actionlog ├── __init__.py ├── apps.py ├── models.py └── signals.py ├── clubadm ├── __init__.py ├── admin.py ├── apps.py ├── auth_backends.py ├── forms.py ├── middleware.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── serializers.py ├── signals.py ├── static │ └── clubadm │ │ └── clubadm.js ├── tasks.py ├── templates │ └── clubadm │ │ ├── notifications │ │ ├── banned.html │ │ ├── gift_received.html │ │ ├── gift_sent.html │ │ └── unbanned.html │ │ └── unsubscribed.html ├── tests.py ├── urls.py └── views.py ├── manage.py ├── oldsanta ├── __init__.py ├── celery.py ├── settings.py ├── static │ ├── images │ │ ├── anonymous.png │ │ ├── background.png │ │ ├── buttons.png │ │ ├── chat.png │ │ ├── chat_input.png │ │ ├── checkbox.png │ │ ├── checkbox2.png │ │ ├── circle.png │ │ ├── decorated.png │ │ ├── favicon.ico │ │ ├── features.png │ │ ├── gift_received.jpg │ │ ├── gift_sent.png │ │ ├── gift_sent2.png │ │ ├── logo.png │ │ ├── nothing_sent.png │ │ ├── novosylov.png │ │ └── stamp.png │ └── less │ │ ├── alert.less │ │ ├── banner.less │ │ ├── button.less │ │ ├── card.less │ │ ├── chat.less │ │ ├── content.less │ │ ├── counters.less │ │ ├── feature.less │ │ ├── footer.less │ │ ├── header.less │ │ ├── logo.less │ │ ├── members.less │ │ ├── profile.less │ │ ├── promo.less │ │ ├── reset.less │ │ ├── scaffolding.less │ │ ├── shipping.less │ │ ├── timetable.less │ │ └── usercontrols.less ├── templates │ ├── clubadm │ │ ├── base.html │ │ ├── notifications │ │ │ ├── giftee_mail.html │ │ │ ├── match.html │ │ │ └── santa_mail.html │ │ ├── profile.html │ │ └── welcome.html │ └── pipeline │ │ ├── css.html │ │ └── js.html ├── urls.py └── wsgi.py ├── package.json └── requirements ├── dev.txt ├── prod.txt └── test.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | .DS_Store 3 | .directory 4 | __pycache__ 5 | node_modules/ 6 | oldsanta-static/ 7 | oldsanta.sqlite3 8 | venv/ 9 | oldsanta/local_settings.py 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - npm install 6 | - pip install -r requirements/test.txt 7 | before_script: 8 | - python manage.py collectstatic --no-input 9 | script: 10 | - python manage.py test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Andrey Privalov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This repository contains the source code of [habra-adm.ru](https://habra-adm.ru). 2 | 3 | This is a very legacy version, yet still in production. A newer version 4 | rewritten in TypeScript from scratch (using Spring MVC on back-end) is coming 5 | soon. 6 | 7 | [![Build Status](https://travis-ci.org/clubadm/clubadm.svg?branch=master)](https://travis-ci.org/clubadm/clubadm) 8 | -------------------------------------------------------------------------------- /actionlog/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "actionlog.apps.ActionLogConfig" 2 | -------------------------------------------------------------------------------- /actionlog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ActionLogConfig(AppConfig): 5 | name = "actionlog" 6 | verbose_name = "Журнал действий" 7 | 8 | def ready(self): 9 | import actionlog.signals 10 | -------------------------------------------------------------------------------- /actionlog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | from clubadm.models import User, Season 5 | 6 | 7 | class LogEntry(models.Model): 8 | LOGGED_IN = 1 9 | LOGGED_OUT = 2 10 | ENROLLED = 3 11 | UNENROLLED = 4 12 | GIFT_SENT = 5 13 | GIFT_RECEIVED = 6 14 | PM_SANTA = 7 15 | PM_GIFTEE = 8 16 | BANNED = 9 17 | UNBANNED = 10 18 | 19 | type = models.IntegerField() # action; shortint 20 | user = models.ForeignKey(User, related_name="actions") # actor 21 | target_user = models.ForeignKey(User, null=True, related_name="actions2") 22 | target_season = models.ForeignKey(Season, null=True) 23 | date = models.DateTimeField(default=timezone.now) 24 | ip_address = models.CharField(max_length=45) 25 | 26 | class Meta: 27 | db_table = "audit_log" 28 | -------------------------------------------------------------------------------- /actionlog/signals.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.signals import user_logged_in, user_logged_out 2 | from django.dispatch import receiver 3 | 4 | from actionlog.models import LogEntry 5 | from clubadm.signals import member_enrolled, member_unenrolled, user_banned, user_unbanned, giftee_mailed, santa_mailed, gift_sent, gift_received 6 | 7 | 8 | @receiver(user_logged_in, dispatch_uid="actionlog.log_user_login") 9 | def log_user_login(sender, user, request, **kwargs): 10 | entry = LogEntry(type=LogEntry.LOGGED_IN, user=user, 11 | ip_address=request.META["REMOTE_ADDR"]) 12 | entry.save() 13 | 14 | 15 | @receiver(user_logged_out, dispatch_uid="actionlog.log_user_logout") 16 | def log_user_logout(sender, user, request, **kwargs): 17 | entry = LogEntry(type=LogEntry.LOGGED_OUT, user=user, 18 | ip_address=request.META["REMOTE_ADDR"]) 19 | entry.save() 20 | 21 | 22 | @receiver(member_enrolled, dispatch_uid="actionlog.log_member_enrollment") 23 | def log_member_enrollment(sender, member, request, **kwargs): 24 | entry = LogEntry(type=LogEntry.ENROLLED, 25 | user=member.user, target_season=member.season, 26 | ip_address=request.META["REMOTE_ADDR"]) 27 | entry.save() 28 | 29 | 30 | @receiver(member_unenrolled, dispatch_uid="actionlog.log_member_unenrollmenet") 31 | def log_member_unenrollment(sender, request, **kwargs): 32 | entry = LogEntry(type=LogEntry.UNENROLLED, 33 | user=request.user, target_season=request.season, 34 | ip_address=request.META["REMOTE_ADDR"]) 35 | entry.save() 36 | 37 | 38 | @receiver(user_banned, dispatch_uid="actionlog.log_user_ban") 39 | def log_user_ban(sender, user, request, **kwargs): 40 | entry = LogEntry(type=LogEntry.BANNED, 41 | user=request.user, target_user=user, 42 | ip_address=request.META["REMOTE_ADDR"]) 43 | entry.save() 44 | 45 | 46 | @receiver(user_unbanned, dispatch_uid="actionlog.log_user_unban") 47 | def log_user_unban(sender, user, request, **kwargs): 48 | entry = LogEntry(type=LogEntry.UNBANNED, 49 | user=request.user, target_user=user, 50 | ip_address=request.META["REMOTE_ADDR"]) 51 | entry.save() 52 | 53 | 54 | @receiver(santa_mailed, dispatch_uid="actionlog.log_santa_mail") 55 | def log_santa_mail(sender, request, **kwargs): 56 | entry = LogEntry(type=LogEntry.PM_SANTA, 57 | user=request.user, target_season=request.season, 58 | ip_address=request.META["REMOTE_ADDR"]) 59 | entry.save() 60 | 61 | 62 | @receiver(giftee_mailed, dispatch_uid="actionlog.log_giftee_mail") 63 | def log_giftee_mail(sender, request, **kwargs): 64 | entry = LogEntry(type=LogEntry.PM_GIFTEE, 65 | user=request.user, target_season=request.season, 66 | ip_address=request.META["REMOTE_ADDR"]) 67 | entry.save() 68 | 69 | 70 | @receiver(gift_received, dispatch_uid="actionlog.log_gift_reception") 71 | def log_gift_reception(sender, request, **kwargs): 72 | entry = LogEntry(type=LogEntry.GIFT_RECEIVED, 73 | user=request.user, target_season=request.season, 74 | ip_address=request.META["REMOTE_ADDR"]) 75 | entry.save() 76 | 77 | 78 | @receiver(gift_sent, dispatch_uid="actionlog.log_gift_shipment") 79 | def log_gift_shipment(sender, request, **kwargs): 80 | entry = LogEntry(type=LogEntry.GIFT_SENT, 81 | user=request.user, target_season=request.season, 82 | ip_address=request.META["REMOTE_ADDR"]) 83 | entry.save() 84 | -------------------------------------------------------------------------------- /clubadm/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = "clubadm.apps.ClubADMConfig" 2 | -------------------------------------------------------------------------------- /clubadm/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.core.cache import cache 3 | from django.core.urlresolvers import reverse 4 | from django.http import Http404, HttpResponseRedirect, JsonResponse 5 | from django.utils.html import format_html 6 | from django.utils.http import urlencode 7 | from django.views.decorators.cache import never_cache 8 | 9 | from clubadm.forms import SeasonForm 10 | from clubadm.models import Member, Season, User 11 | from clubadm.signals import user_banned, user_unbanned 12 | from clubadm.tasks import match_members 13 | 14 | 15 | class SeasonAdmin(admin.ModelAdmin): 16 | actions = ("match_members", "give_badges", "block_members", "clear_cache") 17 | fieldsets = ( 18 | (None, { 19 | "fields": ("year",) 20 | }), 21 | ("Сроки проведения", { 22 | "fields": ("signups_start", "signups_end", "ship_by") 23 | }), 24 | ("Прочее", { 25 | "fields": ("gallery",) 26 | }) 27 | ) 28 | form = SeasonForm 29 | list_display = ("year", "signups_start", "signups_end", "ship_by", 30 | "is_closed", "is_participatable") 31 | ordering = ("year",) 32 | 33 | def get_readonly_fields(self, request, obj=None): 34 | if obj: 35 | return self.readonly_fields + ("year",) 36 | return self.readonly_fields 37 | 38 | def has_delete_permission(self, request, obj=None): 39 | return obj is not None and obj.is_participatable is True 40 | 41 | def is_closed(self, obj): 42 | return obj.is_closed 43 | is_closed.boolean = True 44 | is_closed.short_description = "сезон завершен" 45 | 46 | def is_participatable(self, obj): 47 | return obj.is_participatable 48 | is_participatable.boolean = True 49 | is_participatable.short_description = "регистрация открыта" 50 | 51 | def match_members(self, request, queryset): 52 | for obj in queryset: 53 | match_members.delay(obj.year) 54 | self.message_user(request, "Процесс сортировки был запущен") 55 | match_members.short_description = "Провести жеребьевку" 56 | 57 | def give_badges(self, request, queryset): 58 | array = [] 59 | for obj in queryset: 60 | members = obj.member_set.filter( 61 | gift_sent__isnull=False, 62 | giftee__gift_received__isnull=False) 63 | for member in members: 64 | array.append([ 65 | member.user.username, 66 | member.user_id 67 | ]) 68 | return JsonResponse(array, safe=False) 69 | give_badges.short_description = "Раздать значки \"Дед Мороз\"" 70 | 71 | def block_members(self, request, queryset): 72 | self.message_user(request, "TODO") 73 | block_members.short_description = "Заблокировать плохишей" 74 | 75 | def clear_cache(self, request, queryset): 76 | for obj in queryset: 77 | cache.delete(obj.cache_key) 78 | cache.delete("season:latest") 79 | self.message_user(request, "Кеш успешно очищен") 80 | clear_cache.short_description = "Очистить кеш" 81 | 82 | 83 | class MemberInline(admin.StackedInline): 84 | can_delete = False 85 | fieldsets = ( 86 | (None, { 87 | "fields": ("fullname", "postcode", "address", "is_gift_sent", 88 | "is_gift_received") 89 | }), 90 | ) 91 | max_num = 0 92 | model = Member 93 | readonly_fields = ("fullname", "postcode", "address", "is_gift_sent", 94 | "is_gift_received") 95 | show_change_link = True 96 | verbose_name_plural = "история участия" 97 | 98 | def is_gift_sent(self, obj): 99 | return obj.is_gift_sent 100 | is_gift_sent.boolean = True 101 | is_gift_sent.short_description = "подарок отправлен" 102 | 103 | def is_gift_received(self, obj): 104 | return obj.is_gift_received 105 | is_gift_received.boolean = True 106 | is_gift_received.short_description = "подарок получен" 107 | 108 | 109 | class UserAdmin(admin.ModelAdmin): 110 | actions = ("ban",) 111 | fieldsets = ( 112 | (None, { 113 | "fields": ("username",) 114 | }), 115 | ("Аккаунт", { 116 | "fields": ("get_avatar", "get_karma", "get_rating", "get_rights") 117 | }), 118 | ("Права", { 119 | "fields": ("is_oldfag", "is_banned") 120 | }), 121 | ("Важные даты", { 122 | "fields": ("first_login", "last_login") 123 | }), 124 | ) 125 | search_fields = ("username",) 126 | list_display = ("username", "first_login", "last_login") 127 | readonly_fields = ("username", "get_avatar", "get_karma", "get_rating", 128 | "get_rights", "first_login", "last_login") 129 | inlines = (MemberInline,) 130 | 131 | def has_add_permission(self, request): 132 | return False 133 | 134 | def has_delete_permission(self, request, obj=None): 135 | return False 136 | 137 | def construct_change_message(self, request, form, formsets, add=False): 138 | if add: 139 | return super(UserAdmin, self).construct_change_message( 140 | request, form, formsets, add) 141 | if "is_banned" in form.changed_data: 142 | was_banned = form.initial["is_banned"] 143 | is_banned = form.cleaned_data["is_banned"] 144 | if was_banned and not is_banned: 145 | return "Пользователь разбанен." 146 | if not was_banned and is_banned: 147 | return "Пользователь забанен." 148 | if "is_oldfag" in form.changed_data: 149 | was_oldfag = form.initial["is_oldfag"] 150 | is_oldfag = form.cleaned_data["is_oldfag"] 151 | if was_oldfag and not is_oldfag: 152 | return "Для пользователя введено ограничение по карме." 153 | if not was_oldfag and is_oldfag: 154 | return "Для пользователя снято ограничение по карме." 155 | return super(UserAdmin, self).construct_change_message( 156 | request, form, formsets, add) 157 | 158 | def save_model(self, request, obj, form, change): 159 | if change and "is_banned" in form.changed_data: 160 | was_banned = form.initial["is_banned"] 161 | is_banned = form.cleaned_data["is_banned"] 162 | if was_banned and not is_banned: 163 | user_unbanned.send(sender=User, request=request, user=obj) 164 | elif not was_banned and is_banned: 165 | user_banned.send(sender=User, request=request, user=obj) 166 | super(UserAdmin, self).save_model(request, obj, form, change) 167 | 168 | def view_on_site(self, obj): 169 | return "https://habrahabr.ru/users/%s/" % obj.username 170 | 171 | def ban(self, request, queryset): 172 | queryset.update(is_banned=True) 173 | ban.short_description = "Забанить выбранных пользователей" 174 | 175 | def get_karma(self, obj): 176 | if obj.karma > 0: color = "#6c9007" 177 | elif obj.karma < 0: color = "#d53c30" 178 | else: color = "#c6d4d8" 179 | template = "{}" 180 | return format_html(template, color, obj.karma) 181 | get_karma.short_description = "карма" 182 | 183 | def get_avatar(self, obj): 184 | template = "" 185 | style = "width:32px;height:32px;border-radius:5px" 186 | return format_html(template, obj.avatar, style) 187 | get_avatar.short_description = "аватар" 188 | 189 | def get_rating(self, obj): 190 | template = "{}" 191 | return format_html(template, obj.rating) 192 | get_rating.short_description = "рейтинг" 193 | 194 | def get_rights(self, obj): 195 | if obj.is_readonly: 196 | return "ReadOnly" 197 | if obj.is_readcomment: 198 | return "Read&Comment" 199 | return "Полноценный аккаунт" 200 | get_rights.short_description = "тип аккаунта" 201 | 202 | 203 | class MemberAdmin(admin.ModelAdmin): 204 | fieldsets = ( 205 | (None, { 206 | "fields": ("user", "season", "giftee_link", "santa_link") 207 | }), 208 | ("Почтовый адрес", { 209 | "fields": ("fullname", "postcode", "address") 210 | }), 211 | ("Важные даты", { 212 | "fields": ("gift_sent", "gift_received") 213 | }), 214 | ) 215 | list_display = ("fullname", "season", "is_gift_sent", "is_gift_received") 216 | list_filter = ("season",) 217 | readonly_fields = ("giftee_link", "santa_link") 218 | search_fields = ("fullname",) 219 | 220 | def get_readonly_fields(self, request, obj=None): 221 | if obj: 222 | return self.readonly_fields + ("user", "season") 223 | return self.readonly_fields 224 | 225 | def has_delete_permission(self, request, obj=None): 226 | return obj is not None and obj.giftee is None 227 | 228 | def is_gift_sent(self, obj): 229 | return obj.is_gift_sent 230 | is_gift_sent.boolean = True 231 | is_gift_sent.short_description = "подарок отправлен" 232 | 233 | def is_gift_received(self, obj): 234 | return obj.is_gift_received 235 | is_gift_received.boolean = True 236 | is_gift_received.short_description = "подарок получен" 237 | 238 | def giftee_link(self, obj): 239 | template = "{} ({})" 240 | member_url = reverse("admin:clubadm_member_change", args=[obj.giftee_id]) 241 | user_url = reverse("admin:clubadm_user_change", args=[obj.giftee.user_id]), 242 | return format_html(template, member_url, obj.giftee.fullname, 243 | user_url, obj.giftee.user.username) 244 | giftee_link.short_description = "АПП" 245 | 246 | def santa_link(self, obj): 247 | template = "{} ({})" 248 | member_url = reverse("admin:clubadm_member_change", args=[obj.santa.id]) 249 | user_url = reverse("admin:clubadm_user_change", args=[obj.santa.user_id]) 250 | return format_html(template, member_url, obj.santa.fullname, 251 | user_url, obj.santa.user.username) 252 | santa_link.short_description = "АДМ" 253 | 254 | 255 | class AdminSite(admin.AdminSite): 256 | site_header = "Кабинет Деда Мороза" 257 | site_title = "Клуб анонимных Дедов Морозов" 258 | 259 | def has_permission(self, request): 260 | return request.user.is_authenticated and request.user.is_admin 261 | 262 | @never_cache 263 | def login(self, request, extra_context=None): 264 | index_path = reverse("admin:index", current_app=self.name) 265 | if request.user.is_authenticated: 266 | if request.user.is_admin: 267 | return HttpResponseRedirect(index_path) 268 | raise Http404("Админки не существует") 269 | query_string = urlencode({ 270 | "next": request.GET.get("next", index_path) 271 | }) 272 | return HttpResponseRedirect( 273 | "%s?%s" % (reverse("login"), query_string)) 274 | 275 | 276 | site = AdminSite() 277 | site.register(Season, SeasonAdmin) 278 | site.register(Member, MemberAdmin) 279 | site.register(User, UserAdmin) 280 | -------------------------------------------------------------------------------- /clubadm/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ClubADMConfig(AppConfig): 5 | name = "clubadm" 6 | verbose_name = "Клуб анонимных Дедов Морозов" 7 | -------------------------------------------------------------------------------- /clubadm/auth_backends.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import secrets 3 | 4 | from django.conf import settings 5 | from django.contrib.auth.backends import ModelBackend 6 | from django.core.cache import cache 7 | 8 | from clubadm.models import User 9 | 10 | 11 | class TechMediaBackend(ModelBackend): 12 | def authenticate(self, access_token=None): 13 | remote = self.get_remote_user(access_token) 14 | if not remote: 15 | return None 16 | user_id = remote.get("id") 17 | username = remote.get("login") 18 | changed = False 19 | try: 20 | user = User.objects.get_by_id(user_id) 21 | except User.DoesNotExist: 22 | user = User(pk=user_id, username=username) 23 | user.email_token = secrets.token_urlsafe(24) 24 | changed = True 25 | if user.username != username: 26 | user.username = username 27 | changed = True 28 | if user.access_token != access_token: 29 | user.access_token = access_token 30 | changed = True 31 | if changed: 32 | user.save() 33 | return user 34 | 35 | def get_remote_user(self, access_token): 36 | if not access_token: 37 | return None 38 | user_key = "token:%s" % access_token 39 | user = cache.get(user_key) 40 | if not user: 41 | url = "%s/users/me" % settings.TMAUTH_ENDPOINT_URL 42 | response = requests.get(url, headers={ 43 | "client": settings.TMAUTH_CLIENT, 44 | "token": access_token 45 | }) 46 | if response.status_code != 200: 47 | return False 48 | user = response.json().get("data") 49 | cache.set(user_key, user) 50 | return user 51 | 52 | def get_user(self, user_id): 53 | try: 54 | user = User.objects.get_by_id(user_id) 55 | except: 56 | return None 57 | if not user.remote: 58 | return None 59 | return user 60 | -------------------------------------------------------------------------------- /clubadm/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserChangeForm 3 | 4 | from clubadm.models import User 5 | 6 | 7 | class SeasonForm(forms.ModelForm): 8 | def clean(self): 9 | cleaned_data = super(SeasonForm, self).clean() 10 | 11 | signups_start = cleaned_data.get("signups_start") 12 | signups_end = cleaned_data.get("signups_end") 13 | ship_by = cleaned_data.get("ship_by") 14 | 15 | if signups_end <= signups_start: 16 | self.add_error("signups_end", "Дайте время на регистрацию.") 17 | 18 | if ship_by <= signups_end: 19 | self.add_error("ship_by", "Дайте время на отправку подарка.") 20 | -------------------------------------------------------------------------------- /clubadm/middleware.py: -------------------------------------------------------------------------------- 1 | from django.http import Http404 2 | from django.utils import timezone 3 | 4 | from clubadm.models import Member, Season 5 | 6 | 7 | class SeasonMiddleware(object): 8 | def process_view(self, request, view_func, view_args, view_kwargs): 9 | if "year" in view_kwargs: 10 | year = int(view_kwargs["year"]) 11 | try: 12 | request.season = Season.objects.get_by_year(year) 13 | except Season.DoesNotExist: 14 | raise Http404("Такой сезон еще не создан") 15 | 16 | 17 | class MemberMiddleware(object): 18 | def process_view(self, request, view_func, view_args, view_kwargs): 19 | if "year" in view_kwargs and request.user.is_authenticated: 20 | year = int(view_kwargs["year"]) 21 | try: 22 | request.member = Member.objects.get_by_user_and_year( 23 | request.user, year) 24 | except Member.DoesNotExist: 25 | request.member = None 26 | 27 | 28 | class XUserMiddleware(object): 29 | def process_response(self, request, response): 30 | if not hasattr(request, "user"): 31 | return response 32 | if request.user.is_anonymous: 33 | return response 34 | # Чтобы Nginx мог писать имя пользователя в логи 35 | response["X-User"] = request.user.username 36 | return response 37 | -------------------------------------------------------------------------------- /clubadm/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from django.db import migrations, models 2 | from django.db.models import deletion 3 | from django.utils import timezone 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name="User", 15 | fields=[ 16 | ("id", models.AutoField( 17 | auto_created=True, primary_key=True, serialize=False, 18 | verbose_name="ID")), 19 | ("username", models.CharField( 20 | max_length=25, unique=True, 21 | verbose_name="имя пользователя")), 22 | ("access_token", models.CharField( 23 | blank=True, max_length=40, verbose_name="токен доступа")), 24 | ("is_oldfag", models.BooleanField( 25 | default=False, verbose_name="старый участник", 26 | help_text="Отметьте, чтобы снять ограничение кармы.")), 27 | ("is_banned", models.BooleanField( 28 | default=False, verbose_name="забанен")), 29 | ("first_login", models.DateTimeField( 30 | default=timezone.now, verbose_name="первый вход")), 31 | ("last_login", models.DateTimeField( 32 | blank=True, null=True, verbose_name="последний вход")), 33 | ], 34 | options={ 35 | "verbose_name": "пользователь", 36 | "verbose_name_plural": "пользователи", 37 | "ordering": ["username"], 38 | }, 39 | ), 40 | migrations.CreateModel( 41 | name="Mail", 42 | fields=[ 43 | ("id", models.AutoField( 44 | auto_created=True, primary_key=True, serialize=False, 45 | verbose_name="ID")), 46 | ("body", models.TextField(max_length=400)), 47 | ("send_date", models.DateTimeField( 48 | db_index=True, default=timezone.now)), 49 | ("read_date", models.DateTimeField( 50 | blank=True, db_index=True, null=True)), 51 | ], 52 | options={ 53 | "ordering": ["send_date"], 54 | }, 55 | ), 56 | migrations.CreateModel( 57 | name="Member", 58 | fields=[ 59 | ("id", models.AutoField( 60 | auto_created=True, primary_key=True, serialize=False, 61 | verbose_name="ID")), 62 | ("fullname", models.CharField( 63 | max_length=80, verbose_name="полное имя")), 64 | ("postcode", models.CharField( 65 | max_length=20, verbose_name="индекс")), 66 | ("address", models.TextField( 67 | max_length=200, verbose_name="адрес")), 68 | ("gift_sent", models.DateTimeField( 69 | blank=True, db_index=True, null=True, 70 | verbose_name="подарок отправлен")), 71 | ("gift_received", models.DateTimeField( 72 | blank=True, db_index=True, null=True, 73 | verbose_name="подарок получен")), 74 | ("giftee", models.OneToOneField( 75 | blank=True, null=True, on_delete=deletion.CASCADE, 76 | related_name="santa", to="clubadm.Member", 77 | verbose_name="получатель подарка")), 78 | ], 79 | options={ 80 | "verbose_name": "участник", 81 | "verbose_name_plural": "участники", 82 | "ordering": ["season", "fullname"], 83 | }, 84 | ), 85 | migrations.CreateModel( 86 | name="Season", 87 | fields=[ 88 | ("year", models.IntegerField( 89 | primary_key=True, serialize=False, verbose_name="год")), 90 | ("gallery", models.URLField( 91 | blank=True, verbose_name="пост хвастовства подарками")), 92 | ("signups_start", models.DateField( 93 | verbose_name="начало регистрации")), 94 | ("signups_end", models.DateField( 95 | verbose_name="жеребьевка адресов")), 96 | ("ship_by", models.DateField( 97 | help_text="После этой даты сезон закрывается и уходит вархив.", 98 | verbose_name="последний срок отправки подарка")), 99 | ], 100 | options={ 101 | "verbose_name": "сезон", 102 | "verbose_name_plural": "сезоны", 103 | "ordering": ["year"], 104 | "get_latest_by": "year", 105 | }, 106 | ), 107 | migrations.AddField( 108 | model_name="member", 109 | name="season", 110 | field=models.ForeignKey( 111 | on_delete=deletion.CASCADE, to="clubadm.Season", 112 | verbose_name="сезон"), 113 | ), 114 | migrations.AddField( 115 | model_name="member", 116 | name="user", 117 | field=models.ForeignKey( 118 | on_delete=deletion.CASCADE, to="clubadm.User", 119 | verbose_name="пользователь"), 120 | ), 121 | migrations.AddField( 122 | model_name="mail", 123 | name="recipient", 124 | field=models.ForeignKey( 125 | on_delete=deletion.CASCADE, related_name="+", 126 | to="clubadm.Member"), 127 | ), 128 | migrations.AddField( 129 | model_name="mail", 130 | name="sender", 131 | field=models.ForeignKey( 132 | on_delete=deletion.CASCADE, related_name="+", 133 | to="clubadm.Member"), 134 | ), 135 | migrations.AlterUniqueTogether( 136 | name="member", 137 | unique_together=set([("user", "season")]), 138 | ), 139 | ] 140 | -------------------------------------------------------------------------------- /clubadm/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/clubadm/migrations/__init__.py -------------------------------------------------------------------------------- /clubadm/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.contrib import auth 6 | from django.core.cache import cache 7 | from django.db import models, connection 8 | from django.http import Http404 9 | from django.template.loader import render_to_string 10 | from django.utils import timezone 11 | from django.utils.functional import cached_property 12 | 13 | from clubadm.tasks import send_notification 14 | 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | def _get_mails_cache_key(member1, member2): 20 | # Чтобы один участник мог использовать кэш другого, сортируем параметры 21 | # ключа в порядке возрастания ID, независимо от порядка аргументов. 22 | return "mails:%d:%d" % tuple(sorted([member1.id, member2.id])) 23 | 24 | 25 | class SeasonManager(models.Manager): 26 | def get_queryset(self): 27 | return super(SeasonManager, self).get_queryset().annotate( 28 | members=models.Count("member", distinct=True), 29 | sent=models.Count("member__gift_sent", distinct=True), 30 | received=models.Count("member__gift_received", distinct=True)) 31 | 32 | def get_by_year(self, year): 33 | season_key = "season:%d" % int(year) 34 | season = cache.get(season_key) 35 | if not season: 36 | season = self.get(pk=year) 37 | cache.set(season_key, season, timeout=None) 38 | return season 39 | 40 | 41 | class Season(models.Model): 42 | year = models.IntegerField("год", primary_key=True) 43 | gallery = models.URLField("пост хвастовства подарками", blank=True) 44 | signups_start = models.DateField("начало регистрации") 45 | signups_end = models.DateField("жеребьевка адресов") 46 | ship_by = models.DateField("последний срок отправки подарка", help_text= 47 | "После этой даты сезон закрывается и уходит в" 48 | "архив.") 49 | member_count = models.IntegerField("кол-во участников") 50 | shipped_count = models.IntegerField("сколько отправили") 51 | delivered_count = models.IntegerField("сколько получили") 52 | 53 | objects = SeasonManager() 54 | 55 | class Meta: 56 | ordering = ["year"] 57 | get_latest_by = "year" 58 | verbose_name = "сезон" 59 | verbose_name_plural = "сезоны" 60 | 61 | def __str__(self): 62 | return "АДМ-%d" % self.year 63 | 64 | @property 65 | def cache_key(self): 66 | return "season:%d" % self.pk 67 | 68 | def save(self, *args, **kwargs): 69 | super(Season, self).save(*args, **kwargs) 70 | cache.set(self.cache_key, self, timeout=None) 71 | 72 | def delete(self, *args, **kwargs): 73 | super(Season, self).delete(*args, **kwargs) 74 | cache.delete(self.cache_key) 75 | 76 | def get_absolute_url(self): 77 | return '/%d/' % self.year 78 | 79 | @property 80 | def is_closed(self): 81 | return timezone.now().date() > self.ship_by 82 | 83 | @property 84 | def is_participatable(self): 85 | today = timezone.now().date() 86 | return today >= self.signups_start and today < self.signups_end 87 | 88 | 89 | class UserManager(models.Manager): 90 | def get_by_id(self, user_id): 91 | #user_key = "user:%d" % int(user_id) 92 | #user = cache.get(user_key) 93 | #if not user: 94 | # user = self.get(pk=user_id) 95 | # cache.set(user_key, user, timeout=None) 96 | #return user 97 | return self.get(pk=user_id) 98 | 99 | 100 | class User(models.Model): 101 | GENDER_MALE = 1 102 | GENDER_FEMALE = 2 103 | GENDER_UNKNOWN = 0 104 | 105 | BADGE_ID = 1018 106 | 107 | username = models.CharField("имя пользователя", max_length=25, unique=True) 108 | access_token = models.CharField("токен доступа", blank=True, max_length=40) 109 | email_token = models.CharField("токен для отписки", blank=True, max_length=32) 110 | is_oldfag = models.BooleanField("старый участник", default=False, help_text= 111 | "Отметьте, чтобы снять ограничение кармы.") 112 | is_banned = models.BooleanField("забанен", default=False) 113 | first_login = models.DateTimeField("первый вход", default=timezone.now) 114 | last_login = models.DateTimeField("последний вход", blank=True, null=True) 115 | 116 | is_anonymous = False 117 | is_authenticated = True 118 | 119 | USERNAME_FIELD = "username" 120 | REQUIRED_FIELDS = [] 121 | 122 | objects = UserManager() 123 | 124 | class Meta: 125 | ordering = ["username"] 126 | verbose_name = "пользователь" 127 | verbose_name_plural = "пользователи" 128 | 129 | def __init__(self, *args, **kwargs): 130 | super(User, self).__init__(*args, **kwargs) 131 | self.was_banned = self.is_banned 132 | 133 | def __str__(self): 134 | return self.username 135 | 136 | @property 137 | def cache_key(self): 138 | return "user:%d" % self.id 139 | 140 | def save(self, *args, **kwargs): 141 | super(User, self).save(*args, **kwargs) 142 | cache.set(self.cache_key, self, timeout=None) 143 | if not self.was_banned and self.is_banned: 144 | logger.debug("Пользователь %s забанен", self.username) 145 | self.send_notification("Ваш аккаунт заблокирован", 146 | "clubadm/notifications/banned.html") 147 | elif self.was_banned and not self.is_banned: 148 | logger.debug("Пользователь %s разбанен", self.username) 149 | self.send_notification("Ваш аккаунт разблокирован", 150 | "clubadm/notifications/unbanned.html") 151 | 152 | def delete(self, *args, **kwargs): 153 | super(User, self).delete(*args, **kwargs) 154 | cache.delete(self.cache_key) 155 | 156 | def has_perm(self, perm, obj=None): 157 | return self.is_admin 158 | 159 | def has_module_perms(self, app_label): 160 | return self.is_admin 161 | 162 | def get_username(self): 163 | return self.username 164 | 165 | @property 166 | def is_admin(self): 167 | return settings.DEBUG or self.username in settings.CLUBADM_ADMINS 168 | 169 | @cached_property 170 | def remote(self): 171 | remote = dict() 172 | for backend in auth.get_backends(): 173 | if not hasattr(backend, "get_remote_user"): 174 | continue 175 | try: 176 | remote.update( 177 | backend.get_remote_user(self.access_token)) 178 | except: 179 | pass 180 | return remote 181 | 182 | @property 183 | def avatar(self): 184 | default = "https://habrahabr.ru/i/avatars/stub-user-middle.gif" 185 | # Временный хак, пока разрабы Хабра не исправят API. 186 | url = self.remote.get("avatar", default) 187 | if url == "https://habr.com/images/avatars/stub-user-middle.gif": 188 | return default 189 | return url 190 | 191 | @property 192 | def karma(self): 193 | return float(self.remote.get("score", 0.0)) 194 | 195 | @property 196 | def rating(self): 197 | return float(self.remote.get("rating", 0.0)) 198 | 199 | @property 200 | def gender(self): 201 | return self.remote.get("sex", self.GENDER_UNKNOWN) 202 | 203 | @property 204 | def is_male(self): 205 | return self.gender == self.GENDER_MALE 206 | 207 | @property 208 | def is_female(self): 209 | return self.gender == self.GENDER_FEMALE 210 | 211 | @property 212 | def is_readcomment(self): 213 | return self.remote.get("is_rc", True) 214 | 215 | @property 216 | def is_readonly(self): 217 | return self.remote.get("is_readonly", True) 218 | 219 | @property 220 | def has_badge(self): 221 | badges = self.remote.get("badges") 222 | if not badges: 223 | return False 224 | for badge in badges: 225 | # Временный хак, пока разрабы Хабра не пофиксят свой API: 226 | if isinstance(badge, str): 227 | badge = badges[badge] 228 | if badge.get("id") == self.BADGE_ID: 229 | return True 230 | return False 231 | 232 | @property 233 | def can_participate(self): 234 | invited = not self.is_readonly and not self.is_readcomment 235 | good_guy = self.is_oldfag or self.has_badge 236 | return not self.is_banned and invited and (good_guy or 237 | self.karma >= settings.CLUBADM_KARMA_LIMIT) 238 | 239 | def send_notification(self, title, template_name, context=None): 240 | text = render_to_string(template_name, context) 241 | logger.debug("Отправляю уведомление: title=%s, text=%s", title, text) 242 | send_notification.delay(self.access_token, title, text) 243 | 244 | 245 | class MemberManager(models.Manager): 246 | def get_by_user_and_year(self, user, year): 247 | # FIXME(kafeman): ORM в Django писали какие-то криворукие уроды, поэтому 248 | # мы не можем получить АДМ и АПП в одном запросе, даже через RawSQL, 249 | # потому что эти дебилы не могут правильно распарсить готовый результат. 250 | queryset = self.select_related("giftee").prefetch_related("santa") 251 | return queryset.get(user=user, season_id=year) 252 | 253 | 254 | class Member(models.Model): 255 | user = models.ForeignKey(User, verbose_name="пользователь") 256 | season = models.ForeignKey(Season, verbose_name="сезон") 257 | giftee = models.OneToOneField("self", related_name="santa", 258 | verbose_name="получатель подарка", 259 | blank=True, null=True) 260 | fullname = models.CharField("полное имя", max_length=80) 261 | postcode = models.CharField("индекс", max_length=20) 262 | address = models.TextField("адрес", max_length=200) 263 | gift_sent = models.DateTimeField("подарок отправлен", db_index=True, 264 | blank=True, null=True) 265 | gift_received = models.DateTimeField("подарок получен", db_index=True, 266 | blank=True, null=True) 267 | 268 | objects = MemberManager() 269 | 270 | class Meta: 271 | verbose_name = "участник" 272 | verbose_name_plural = "участники" 273 | ordering = ["season", "fullname"] 274 | unique_together = ("user", "season") 275 | 276 | def __str__(self): 277 | return "%s (%s)" % (self.fullname, self.season) 278 | 279 | @property 280 | def is_gift_sent(self): 281 | return self.gift_sent is not None 282 | 283 | @property 284 | def is_gift_received(self): 285 | return self.gift_received is not None 286 | 287 | def read_mails(self, sender, timestamp): 288 | Mail.objects.filter( 289 | sender=sender, 290 | recipient=self, 291 | read_date__isnull=True, 292 | send_date__lte=timezone.make_aware(datetime.datetime.fromtimestamp(timestamp)), 293 | send_date__gte=timezone.make_aware(datetime.datetime(2016, 12, 20)) 294 | ).update(read_date=timezone.now()) 295 | cache.delete(_get_mails_cache_key(self, sender)) 296 | 297 | def send_mail(self, body, recipient): 298 | mail = Mail(body=body, sender=self, recipient=recipient) 299 | mail.save() 300 | 301 | def send_gift(self): 302 | self.gift_sent = timezone.now() 303 | self.save() 304 | 305 | def receive_gift(self): 306 | self.gift_received = timezone.now() 307 | self.save() 308 | 309 | 310 | class MailManager(models.Manager): 311 | def get_between(self, member1, member2): 312 | mails_key = _get_mails_cache_key(member1, member2) 313 | mails = cache.get(mails_key) 314 | if not mails: 315 | mails = list(self.filter( 316 | models.Q(sender=member1, recipient=member2) | 317 | models.Q(recipient=member1, sender=member2) 318 | )) 319 | cache.set(mails_key, mails, timeout=None) 320 | return mails 321 | 322 | 323 | class Mail(models.Model): 324 | body = models.TextField(max_length=400) 325 | sender = models.ForeignKey(Member, related_name="+") 326 | recipient = models.ForeignKey(Member, related_name="+") 327 | send_date = models.DateTimeField(default=timezone.now, db_index=True) 328 | read_date = models.DateTimeField(blank=True, null=True, db_index=True) 329 | 330 | objects = MailManager() 331 | 332 | class Meta: 333 | ordering = ["send_date"] 334 | 335 | def save(self, *args, **kwargs): 336 | super(Mail, self).save(*args, **kwargs) 337 | cache.delete(_get_mails_cache_key(self.sender, self.recipient)) 338 | -------------------------------------------------------------------------------- /clubadm/serializers.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | 3 | from rest_framework import serializers 4 | 5 | from clubadm.models import Member, Season, Mail, User 6 | 7 | 8 | class UserSerializer(serializers.ModelSerializer): 9 | avatar = serializers.SerializerMethodField() 10 | is_active = serializers.SerializerMethodField() 11 | can_participate = serializers.SerializerMethodField() 12 | 13 | class Meta: 14 | model = User 15 | fields = ("username", "avatar", "is_active", "can_participate") 16 | 17 | def get_avatar(self, obj): 18 | return obj.avatar 19 | 20 | def get_is_active(self, obj): 21 | return not obj.is_banned 22 | 23 | def get_can_participate(self, obj): 24 | return obj.can_participate 25 | 26 | 27 | class SeasonSerializer(serializers.HyperlinkedModelSerializer): 28 | members = serializers.SerializerMethodField() 29 | sent = serializers.SerializerMethodField() 30 | received = serializers.SerializerMethodField() 31 | timeleft = serializers.SerializerMethodField() 32 | is_closed = serializers.SerializerMethodField() 33 | is_participatable = serializers.SerializerMethodField() 34 | gallery = serializers.SerializerMethodField() 35 | 36 | class Meta: 37 | model = Season 38 | fields = ("year", "signups_start", "signups_end", "ship_by", "members", 39 | "sent", "received", "timeleft", "is_closed", 40 | "is_participatable", "gallery") 41 | 42 | def get_members(self, obj): 43 | return obj.members 44 | 45 | def get_sent(self, obj): 46 | return obj.sent 47 | 48 | def get_received(self, obj): 49 | return obj.received 50 | 51 | def get_timeleft(self, obj): 52 | timeleft = (obj.signups_end - date.today()).days 53 | if timeleft > 0: 54 | return timeleft 55 | return None 56 | 57 | def get_is_closed(self, obj): 58 | return obj.is_closed 59 | 60 | def get_is_participatable(self, obj): 61 | return obj.is_participatable 62 | 63 | def get_gallery(self, obj): 64 | if obj.gallery: 65 | return obj.gallery 66 | return None 67 | 68 | 69 | class GifteeSerializer(serializers.ModelSerializer): 70 | unread = serializers.SerializerMethodField() 71 | mails = serializers.SerializerMethodField() 72 | 73 | class Meta: 74 | model = Member 75 | fields = ("fullname", "postcode", "address", "unread", "mails", 76 | "is_gift_received") 77 | 78 | def get_unread(self, obj): 79 | import datetime 80 | from django.utils import timezone 81 | return Mail.objects.filter( 82 | sender=obj, 83 | recipient=obj.santa, 84 | read_date__isnull=True, 85 | send_date__gte=timezone.make_aware(datetime.datetime(2016, 12, 20)) 86 | ).count() 87 | 88 | def get_mails(self, obj): 89 | mails = Mail.objects.get_between(obj, obj.santa) 90 | return MailSerializer(mails, context={ 91 | "author_id": obj.santa.id 92 | }, many=True).data 93 | 94 | 95 | class SantaSerializer(serializers.ModelSerializer): 96 | unread = serializers.SerializerMethodField() 97 | mails = serializers.SerializerMethodField() 98 | 99 | class Meta: 100 | model = Member 101 | fields = ("unread", "mails", "is_gift_sent") 102 | 103 | def get_unread(self, obj): 104 | import datetime 105 | from django.utils import timezone 106 | return Mail.objects.filter( 107 | sender=obj, 108 | recipient=obj.giftee, 109 | read_date__isnull=True, 110 | send_date__gte=timezone.make_aware(datetime.datetime(2016, 12, 20)) 111 | ).count() 112 | 113 | def get_mails(self, obj): 114 | mails = Mail.objects.get_between(obj, obj.giftee) 115 | return MailSerializer(mails, context={ 116 | "author_id": obj.giftee_id 117 | }, many=True).data 118 | 119 | 120 | class MemberSerializer(serializers.ModelSerializer): 121 | giftee = GifteeSerializer(read_only=True) 122 | santa = SantaSerializer(read_only=True) 123 | 124 | class Meta: 125 | model = Member 126 | fields = ("fullname", "postcode", "address", "giftee", "santa", 127 | "is_gift_sent", "is_gift_received") 128 | read_only_fields = ("is_gift_sent", "is_gift_received") 129 | 130 | def validate_fullname(self, value): 131 | if " " not in value.strip(): 132 | # Надоели придурки, которые в поле "Полное имя" пишут "Вася". 133 | raise serializers.ValidationError("Вы должны ввести полное имя") 134 | return value 135 | 136 | 137 | class MailSerializer(serializers.ModelSerializer): 138 | is_author = serializers.SerializerMethodField() 139 | 140 | class Meta: 141 | model = Mail 142 | fields = ("is_author", "body", "send_date", "read_date") 143 | 144 | def get_is_author(self, obj): 145 | return obj.sender_id == self.context["author_id"] 146 | -------------------------------------------------------------------------------- /clubadm/signals.py: -------------------------------------------------------------------------------- 1 | from django.dispatch import Signal 2 | 3 | 4 | member_enrolled = Signal() 5 | member_unenrolled = Signal() 6 | 7 | user_banned = Signal() 8 | user_unbanned = Signal() 9 | 10 | giftee_mailed = Signal() 11 | santa_mailed = Signal() 12 | 13 | gift_sent = Signal() 14 | gift_received = Signal() 15 | -------------------------------------------------------------------------------- /clubadm/static/clubadm/clubadm.js: -------------------------------------------------------------------------------- 1 | (function(window, angular) { 2 | 3 | "use strict"; 4 | 5 | var clubadm = angular.module("clubadm", ["angularMoment", "luegg.directives"]); 6 | 7 | clubadm.run(["$rootScope", "amMoment", function($rootScope, amMoment) { 8 | amMoment.changeLocale("ru"); 9 | }]); 10 | 11 | clubadm.config(["$httpProvider", function($httpProvider) { 12 | $httpProvider.defaults.headers.common["X-CSRFToken"] = window.csrf_token; 13 | }]); 14 | 15 | clubadm.controller("ProfileController", ["$scope", "$http", function($scope, $http) { 16 | var lastUpdate = 0; 17 | $scope.chat = {}; 18 | 19 | function update(data) { 20 | lastUpdate = Math.floor(Date.now() / 1000); 21 | $scope.season = data.season; 22 | $scope.member = data.member; 23 | 24 | if (data.user) { 25 | $scope.user = data.user; 26 | } 27 | 28 | if (data.member) { 29 | $scope.form = angular.copy(data.member); 30 | } else { 31 | $scope.form = {}; 32 | } 33 | } 34 | 35 | function readMails(year, sender, timestamp) { 36 | $http({ 37 | url: "/" + year + "/read_mails/", 38 | method: "POST", 39 | data: "sender=" + sender + "×tamp=" + timestamp, 40 | headers: { 41 | "Content-Type": "application/x-www-form-urlencoded" 42 | } 43 | }).then(function(response) { 44 | update(response.data); 45 | }, function(response) { 46 | alert(response.data.error); 47 | }); 48 | } 49 | 50 | $scope.$watch("flippers.santa", function(newValue, oldValue) { 51 | if (oldValue == false && newValue == true) { 52 | readMails($scope.season.year, "santa", lastUpdate); 53 | } 54 | }); 55 | 56 | $scope.$watch("flippers.giftee", function(newValue, oldValue) { 57 | if (oldValue == false && newValue == true) { 58 | readMails($scope.season.year, "giftee", lastUpdate); 59 | } 60 | }); 61 | 62 | $scope.signUp = function() { 63 | var data = [ 64 | "fullname=", encodeURIComponent($scope.form.fullname), 65 | "&postcode=", encodeURIComponent($scope.form.postcode), 66 | "&address=", encodeURIComponent($scope.form.address), 67 | ]; 68 | 69 | $http({ 70 | url: "/" + $scope.season.year + "/signup/", 71 | method: "POST", 72 | data: data.join(""), 73 | headers: { 74 | "Content-Type": "application/x-www-form-urlencoded" 75 | } 76 | }).then(function(response) { 77 | update(response.data); 78 | }, function(response) { 79 | alert(response.data.error); 80 | }); 81 | }; 82 | 83 | $scope.signOut = function() { 84 | var url = "/" + $scope.season.year + "/signout/"; 85 | $http.post(url).then(function(response) { 86 | update(response.data); 87 | }, function(response) { 88 | alert(response.data.error); 89 | }); 90 | }; 91 | 92 | $scope.sendGift = function() { 93 | var url = "/" + $scope.season.year + "/send_gift/"; 94 | $http.post(url).then(function(response) { 95 | update(response.data); 96 | }, function(response) { 97 | alert(response.data.error); 98 | }); 99 | }; 100 | 101 | $scope.receiveGift = function() { 102 | var url = "/" + $scope.season.year + "/receive_gift/"; 103 | $http.post(url).then(function(response) { 104 | update(response.data); 105 | }, function(response) { 106 | alert(response.data.error); 107 | }); 108 | }; 109 | 110 | $scope.sendMail = function(recipient) { 111 | if (recipient == "giftee") { 112 | var message = $scope.chat.giftee_message; 113 | $scope.chat.giftee_message = ""; 114 | } else { 115 | var message = $scope.chat.santa_message; 116 | $scope.chat.santa_message = ""; 117 | } 118 | $http({ 119 | url: "/" + $scope.season.year + "/send_mail/", 120 | method: "POST", 121 | data: "recipient=" + recipient + "&body=" + encodeURIComponent(message), 122 | headers: { 123 | "Content-Type": "application/x-www-form-urlencoded" 124 | } 125 | }).then(function(response) { 126 | update(response.data); 127 | }, function(response) { 128 | alert(response.data.error); 129 | }); 130 | }; 131 | 132 | update(window.prefetched); 133 | }]); 134 | 135 | })(window, window.angular); 136 | -------------------------------------------------------------------------------- /clubadm/tasks.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | 4 | from django.conf import settings 5 | 6 | from celery import shared_task 7 | 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | @shared_task 13 | def match_members(year): 14 | from clubadm.models import Member 15 | # TODO(kafeman): Придумать что-нибудь сложнее и интереснее. 16 | members = Member.objects.filter(season_id=year).order_by("?") 17 | last = len(members) - 1 18 | for i, member in enumerate(members): 19 | if i == last: 20 | giftee = members[0] 21 | else: 22 | giftee = members[i + 1] 23 | member.giftee = giftee 24 | member.save() 25 | logger.debug("%s отправляет подарок %s", member, giftee) 26 | try: 27 | member.user.send_notification( 28 | "Пора отправлять подарок!", 29 | "clubadm/notifications/match.html", { 30 | "year": year 31 | }) 32 | except: 33 | logger.warning("Не удалось отправить уведомление %s", member) 34 | 35 | 36 | @shared_task 37 | def send_notification(access_token, title, text): 38 | url = "%s/tracker" % settings.TMAUTH_ENDPOINT_URL 39 | headers = { 40 | "client": settings.TMAUTH_CLIENT, 41 | "token": access_token 42 | } 43 | data = { 44 | "title": title, 45 | "text": text 46 | } 47 | response = requests.put(url, headers=headers, data=data) 48 | response.raise_for_status() 49 | -------------------------------------------------------------------------------- /clubadm/templates/clubadm/notifications/banned.html: -------------------------------------------------------------------------------- 1 | Для выяснения причин свяжитесь с пользователем @clubadm. 2 | -------------------------------------------------------------------------------- /clubadm/templates/clubadm/notifications/gift_received.html: -------------------------------------------------------------------------------- 1 | Ваш получатель отметил на сайте, что подарок получен. Это круто! 2 | -------------------------------------------------------------------------------- /clubadm/templates/clubadm/notifications/gift_sent.html: -------------------------------------------------------------------------------- 1 | Дед Мороз отметил на сайте, что подарок уже в пути! Пожалуйста, не забудьте отметить, когда он придет. 2 | -------------------------------------------------------------------------------- /clubadm/templates/clubadm/notifications/unbanned.html: -------------------------------------------------------------------------------- 1 | Желаем вам счастливого Нового Года и Рождества! :-) 2 | -------------------------------------------------------------------------------- /clubadm/templates/clubadm/unsubscribed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

На адрес {{ email }} больше не будут поступать уведомления от нас.

4 |

We will no longer send notifications to {{ email }}.

5 | -------------------------------------------------------------------------------- /clubadm/tests.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.core.cache import cache 4 | from django.test import TestCase, Client 5 | from django.utils import timezone 6 | 7 | from clubadm.models import Season, Member 8 | 9 | 10 | client = Client() 11 | 12 | 13 | class SeasonMethodTests(TestCase): 14 | def get_future_date(self): 15 | time = timezone.now() + datetime.timedelta(days=1) 16 | return time.date() 17 | 18 | def get_past_date(self): 19 | time = timezone.now() - datetime.timedelta(days=1) 20 | return time.date() 21 | 22 | def get_present_date(self): 23 | return timezone.now().date() 24 | 25 | def test_is_closed(self): 26 | season = Season(ship_by=self.get_future_date()) 27 | self.assertEqual(season.is_closed, False) 28 | 29 | season = Season(ship_by=self.get_past_date()) 30 | self.assertEqual(season.is_closed, True) 31 | 32 | season = Season(ship_by=self.get_present_date()) 33 | self.assertEqual(season.is_closed, False) 34 | 35 | def test_is_participatable(self): 36 | season = Season(signups_start=self.get_future_date(), 37 | signups_end=self.get_future_date()) 38 | self.assertEqual(season.is_participatable, False) 39 | 40 | season = Season(signups_start=self.get_future_date(), 41 | signups_end=self.get_past_date()) 42 | self.assertEqual(season.is_participatable, False) 43 | 44 | season = Season(signups_start=self.get_future_date(), 45 | signups_end=self.get_present_date()) 46 | self.assertEqual(season.is_participatable, False) 47 | 48 | season = Season(signups_start=self.get_past_date(), 49 | signups_end=self.get_future_date()) 50 | self.assertEqual(season.is_participatable, True) 51 | 52 | season = Season(signups_start=self.get_past_date(), 53 | signups_end=self.get_past_date()) 54 | self.assertEqual(season.is_participatable, False) 55 | 56 | season = Season(signups_start=self.get_past_date(), 57 | signups_end=self.get_present_date()) 58 | self.assertEqual(season.is_participatable, False) 59 | 60 | season = Season(signups_start=self.get_present_date(), 61 | signups_end=self.get_future_date()) 62 | self.assertEqual(season.is_participatable, True) 63 | 64 | season = Season(signups_start=self.get_present_date(), 65 | signups_end=self.get_past_date()) 66 | self.assertEqual(season.is_participatable, False) 67 | 68 | season = Season(signups_start=self.get_present_date(), 69 | signups_end=self.get_present_date()) 70 | self.assertEqual(season.is_participatable, False) 71 | 72 | 73 | class MemberMethodTests(TestCase): 74 | def test_is_gift_sent(self): 75 | member = Member(gift_sent=None) 76 | self.assertEqual(member.is_gift_sent, False) 77 | 78 | member = Member(gift_sent=timezone.now()) 79 | self.assertEqual(member.is_gift_sent, True) 80 | 81 | def test_is_gift_received(self): 82 | member = Member(gift_received=None) 83 | self.assertEqual(member.is_gift_received, False) 84 | 85 | member = Member(gift_received=timezone.now()) 86 | self.assertEqual(member.is_gift_received, True) 87 | 88 | 89 | class OldProfileViewTests(TestCase): 90 | def test_adm2012(self): 91 | season = Season(year=2012, 92 | signups_start=timezone.now().date(), 93 | signups_end=timezone.now().date(), 94 | ship_by=timezone.now().date()) 95 | season.save() 96 | cache.delete(season.cache_key) 97 | response = self.client.get( 98 | "/profile?id=1&hash=d5c768022c7a512815653d9ae541964b") 99 | #self.assertRedirects(response, "/2012/profile/") 100 | 101 | def test_adm2013(self): 102 | season = Season(year=2013, 103 | signups_start=timezone.now().date(), 104 | signups_end=timezone.now().date(), 105 | ship_by=timezone.now().date()) 106 | season.save() 107 | cache.delete(season.cache_key) 108 | response = self.client.get("/profile") 109 | #self.assertRedirects(response, "/2013/profile/") 110 | -------------------------------------------------------------------------------- /clubadm/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import include, url 3 | from django.contrib.auth.views import logout 4 | 5 | from clubadm import views, admin 6 | 7 | 8 | urlpatterns = [ 9 | url(r"^$", views.home, name="home"), 10 | url(r"^login$", views.login, name="login"), 11 | url(r"^callback$", views.callback, name="callback"), 12 | url(r"^(?P[0-9]{4})/$", views.welcome, name="welcome"), 13 | url(r"^(?P[0-9]{4})/signup/$", views.signup, name="signup"), 14 | url(r"^(?P[0-9]{4})/signout/$", views.signout, name="signout"), 15 | url(r"^(?P[0-9]{4})/profile/$", views.profile, name="profile"), 16 | url(r"^(?P[0-9]{4})/send_mail/$", views.send_mail, name="send_mail"), 17 | url(r"^(?P[0-9]{4})/send_gift/$", views.send_gift, name="send_gift"), 18 | url(r"^(?P[0-9]{4})/receive_gift/$", views.receive_gift, name="receive_gift"), 19 | url(r"^(?P[0-9]{4})/read_mails/$", views.read_mails, name="read_mails"), 20 | url(r"^logout$", logout, {"next_page": "/"}), 21 | url(r"^profile$", views.profile_legacy), 22 | #url(r"^admin/", admin.site.urls), 23 | url(r"^jserror$", views.jserror, name="jserror"), 24 | url(r"^unsubscribe$", views.unsubscribe), 25 | ] 26 | 27 | 28 | if settings.DEBUG: 29 | import debug_toolbar 30 | urlpatterns += [ 31 | url(r"^__debug__/", include(debug_toolbar.urls)), 32 | ] 33 | -------------------------------------------------------------------------------- /clubadm/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import requests 4 | import html 5 | 6 | from django.conf import settings 7 | from django.contrib.auth import authenticate, login as auth_login 8 | from django.core.cache import cache 9 | from django.core.urlresolvers import reverse 10 | from django.http import Http404, HttpResponse, HttpResponseRedirect 11 | from django.shortcuts import redirect, render 12 | from django.utils import timezone 13 | from django.utils.http import urlencode 14 | from django.middleware.csrf import get_token 15 | from django.db.models import F 16 | 17 | from clubadm.models import Season, Member, Mail 18 | from clubadm.serializers import SeasonSerializer, MemberSerializer, UserSerializer 19 | from clubadm.signals import member_enrolled, member_unenrolled, giftee_mailed, santa_mailed, gift_sent, gift_received 20 | 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class _AjaxException(Exception): 26 | pass 27 | 28 | 29 | class _AjaxResponse(HttpResponse): 30 | def __init__(self, data, status=200): 31 | content = json.dumps(data, ensure_ascii=False, separators=(",", ": "), indent=2) 32 | super(_AjaxResponse, self).__init__( 33 | content=content, status=status, charset="utf-8", 34 | content_type="application/json;charset=utf-8") 35 | 36 | 37 | def _ajax_view(member_required=False, match_required=False): 38 | def decorator(func): 39 | def view(request, *args, **kwargs): 40 | if request.method != "POST": 41 | return _AjaxResponse({ 42 | "error": "Используйте POST, пожалуйста" 43 | }, status=405) 44 | if request.user.is_anonymous: 45 | return _AjaxResponse({ 46 | "error": "Представьтесь, пожалуйста" 47 | }, status=403) 48 | if member_required and not request.member: 49 | return _AjaxResponse({ 50 | "error": "Ой, а вы во всем этом и не участвуете" 51 | }, status=403) 52 | if match_required and not request.member.giftee_id: 53 | return _AjaxResponse({ 54 | "error": "Давайте дождемся жеребьевки, а там посмотрим" 55 | }, status=403) 56 | try: 57 | return func(request) 58 | except _AjaxException as e: 59 | return _AjaxResponse({ 60 | "error": str(e) 61 | }, status=400) 62 | return view 63 | return decorator 64 | 65 | 66 | def _create_login_url(request, next): 67 | redirect_uri = "{callback}?{params}".format( 68 | callback=reverse("callback"), 69 | params=urlencode({ "next": next }) 70 | ) 71 | return "{login_url}?{params}".format( 72 | login_url=settings.TMAUTH_LOGIN_URL, 73 | params=urlencode({ 74 | "redirect_uri": request.build_absolute_uri(redirect_uri), 75 | "client_id": settings.TMAUTH_CLIENT, 76 | "response_type": "code", 77 | "state": get_token(request), 78 | }) 79 | ) 80 | 81 | 82 | def _send_email(user_id, subject, body): 83 | requests.post("http://192.168.15.20:8080/users/{}/email".format(user_id), json={ 84 | "subject": subject, 85 | "body": body, 86 | }, headers={ 87 | "X-Dumb-CSRF-Protection": "yes", 88 | }) 89 | 90 | 91 | def home(request): 92 | try: 93 | season = Season.objects.latest() 94 | except Season.DoesNotExist: 95 | return redirect("admin:clubadm_season_add") 96 | if request.user.is_authenticated: 97 | return redirect("profile", year=season.year) 98 | return redirect("welcome", year=season.year) 99 | 100 | 101 | def welcome(request, year): 102 | if request.user.is_authenticated: 103 | return redirect("profile", year=year) 104 | login_url = _create_login_url(request, '/{}/profile/'.format(year)) 105 | return render(request, "clubadm/welcome.html", { 106 | "season": request.season, 107 | "login_url": login_url, 108 | }) 109 | 110 | 111 | def profile(request, year): 112 | if request.user.is_anonymous: 113 | return redirect("welcome", year=year) 114 | 115 | prefetched = { 116 | "season": SeasonSerializer(request.season).data, 117 | "user": UserSerializer(request.user).data, 118 | "member": None, 119 | } 120 | 121 | if request.member: 122 | prefetched["member"] = MemberSerializer(request.member).data 123 | 124 | return render(request, "clubadm/profile.html", { 125 | "season": request.season, 126 | "prefetched": html.escape(json.dumps(prefetched, ensure_ascii=False), quote=False), 127 | }) 128 | 129 | 130 | def profile_legacy(request): 131 | if "hash" in request.GET: 132 | return redirect("profile", year=2012) 133 | return redirect("profile", year=2013) 134 | 135 | 136 | def login(request): 137 | next = request.GET.get("next", reverse("home")) 138 | return HttpResponseRedirect(_create_login_url(request, next)) 139 | 140 | 141 | def callback(request): 142 | redirect_to = request.GET.get("next", reverse("home")) 143 | 144 | if request.user.is_authenticated: 145 | return HttpResponseRedirect(redirect_to) 146 | 147 | if "error" in request.GET: 148 | return redirect("home") 149 | 150 | response = requests.post(settings.TMAUTH_TOKEN_URL, data={ 151 | "grant_type": "authorization_code", 152 | "code": request.GET.get("code"), 153 | "client_id": settings.TMAUTH_CLIENT, 154 | "client_secret": settings.TMAUTH_SECRET 155 | }) 156 | 157 | if response.status_code != 200: 158 | logger.warning(response.text) 159 | return HttpResponse("Хабр вернул ошибку. Попробуйте снова.", 160 | content_type="text/plain;charset=utf-8") 161 | 162 | user = authenticate(access_token=response.json().get("access_token")) 163 | auth_login(request, user) 164 | 165 | return HttpResponseRedirect(redirect_to) 166 | 167 | 168 | def unsubscribe(request): 169 | user_id = int(request.GET.get("uid")) 170 | response = requests.post("http://192.168.15.20:8080/users/{}/unsubscribe".format(user_id), json={ 171 | "token": request.GET.get("token"), 172 | }, headers={ 173 | "X-Dumb-CSRF-Protection": "yes", 174 | "X-Real-IP": request.META["REMOTE_ADDR"], 175 | }) 176 | if response.status_code != 200: 177 | logger.warning(response.text) 178 | return HttpResponse("Мы честно пытались отписать вас, но произошла ошибка. Напишите, пожалуйста, .", 179 | content_type="text/plain;charset=utf-8") 180 | return render(request, "clubadm/unsubscribed.html", { 181 | "email": response.json().get("email"), 182 | }) 183 | 184 | 185 | @_ajax_view(member_required=False, match_required=False) 186 | def signup(request): 187 | if not request.season.is_participatable: 188 | raise _AjaxException("Регистрация на этот сезон не возможна") 189 | if request.member: 190 | raise _AjaxException("Вы уже зарегистрированы на этот сезон") 191 | if not request.user.can_participate: 192 | raise _AjaxException("Вы не можете участвовать в нашем клубе") 193 | serializer = MemberSerializer(data=request.POST) 194 | if not serializer.is_valid(): 195 | raise _AjaxException("Форма заполнена неверно") 196 | member = serializer.save(season=request.season, user=request.user) 197 | cache.delete(request.season.cache_key) 198 | request.season.members += 1 199 | member_enrolled.send(sender=Member, request=request, member=member) 200 | return _AjaxResponse({ 201 | "season": SeasonSerializer(request.season).data, 202 | "member": serializer.data, 203 | }) 204 | 205 | 206 | @_ajax_view(member_required=True, match_required=False) 207 | def signout(request): 208 | if not request.season.is_participatable or request.member.giftee_id: 209 | raise _AjaxException("Время на решение истекло") 210 | request.member.delete() 211 | cache.delete(request.season.cache_key) 212 | request.season.members -= 1 213 | member_unenrolled.send(sender=Member, request=request) 214 | return _AjaxResponse({ 215 | "season": SeasonSerializer(request.season).data, 216 | "member": None, 217 | }) 218 | 219 | 220 | @_ajax_view(member_required=True, match_required=True) 221 | def send_mail(request): 222 | if request.season.is_closed: 223 | raise _AjaxException("Этот сезон находится в архиве") 224 | body = request.POST.get("body", "") 225 | if not body.strip(): 226 | raise _AjaxException("Вначале нужно что-то написать") 227 | recipient = request.POST.get("recipient", "") 228 | if recipient == "giftee": 229 | request.member.send_mail(body, request.member.giftee) 230 | request.member.giftee.user.send_notification( 231 | "Новое сообщение от Деда Мороза", 232 | "clubadm/notifications/santa_mail.html", { 233 | "season": request.season 234 | } 235 | ) 236 | _send_email(request.member.giftee.user.id, "Новое сообщение от Деда Мороза", 237 | "Здравствуйте, {}!\n\nВаш Дед Мороз написал вам что-то в анонимном чатике! ".format(request.member.giftee.user.username) + 238 | "Посмотреть сообщение можно в профиле: https://habra-adm.ru/{}/profile/".format(request.season.year)) 239 | giftee_mailed.send(sender=Member, request=request) 240 | elif recipient == "santa": 241 | request.member.send_mail(body, request.member.santa) 242 | request.member.santa.user.send_notification( 243 | "Новое сообщение от получателя подарка", 244 | "clubadm/notifications/giftee_mail.html", { 245 | "season": request.season 246 | } 247 | ) 248 | _send_email(request.member.santa.user.id, "Новое сообщение от получателя подарка", 249 | "Привет, Дед Мороз {}!\n\nВаш получатель написал вам что-то в анонимном чатике! ".format(request.member.santa.user.username) + 250 | "Посмотреть сообщение можно в профиле: https://habra-adm.ru/{}/profile/".format(request.season.year)) 251 | santa_mailed.send(sender=Member, request=request) 252 | else: 253 | raise _AjaxException("Неизвестный получатель") 254 | return _AjaxResponse({ 255 | "season": SeasonSerializer(request.season).data, 256 | "member": MemberSerializer(request.member).data, 257 | }) 258 | 259 | 260 | @_ajax_view(member_required=True, match_required=True) 261 | def read_mails(request): 262 | sender = request.POST.get("sender", "") 263 | timestamp = request.POST.get("timestamp", 0.0) 264 | try: 265 | timestamp = float(timestamp) 266 | except ValueError: 267 | raise _AjaxException("Нахрена тут строка?") 268 | if sender == "giftee": 269 | request.member.read_mails(request.member.giftee, timestamp) 270 | elif sender == "santa": 271 | request.member.read_mails(request.member.santa, timestamp) 272 | else: 273 | raise _AjaxException("Неизвестный отправитель") 274 | return _AjaxResponse({ 275 | "season": SeasonSerializer(request.season).data, 276 | "member": MemberSerializer(request.member).data, 277 | }) 278 | 279 | 280 | @_ajax_view(member_required=True, match_required=True) 281 | def send_gift(request): 282 | if request.season.is_closed: 283 | raise _AjaxException("Этот сезон находится в архиве") 284 | if request.member.is_gift_sent: 285 | raise _AjaxException("Вами уже был отправлен один подарок") 286 | request.member.send_gift() 287 | cache.delete(request.season.cache_key) 288 | request.season.sent += 1 289 | request.member.giftee.user.send_notification( 290 | "Вам отправлен подарок", "clubadm/notifications/gift_sent.html") 291 | _send_email(request.member.giftee.user.id, "Вам отправлен подарок", 292 | "Здравствуйте, {}!\n\nВаш Дед Мороз отметил на сайте, что подарок уже в пути. ".format(request.member.giftee.user.username) + 293 | "Не забудьте и вы отметить на сайте, когда он придет!") 294 | Season.objects.filter(year = request.season.year).update(shipped_count = F("shipped_count") + 1) 295 | gift_sent.send(sender=Member, request=request) 296 | return _AjaxResponse({ 297 | "season": SeasonSerializer(request.season).data, 298 | "member": MemberSerializer(request.member).data, 299 | }) 300 | 301 | 302 | @_ajax_view(member_required=True, match_required=True) 303 | def receive_gift(request): 304 | if request.season.is_closed: 305 | raise _AjaxException("Этот сезон находится в архиве") 306 | if request.member.is_gift_received: 307 | raise _AjaxException("Вами уже был получен один подарок") 308 | request.member.receive_gift() 309 | cache.delete(request.season.cache_key) 310 | request.season.received += 1 311 | request.member.santa.user.send_notification( 312 | "Ваш подарок получен", "clubadm/notifications/gift_received.html") 313 | _send_email(request.member.santa.user.id, "Ваш подарок получен", 314 | "Привет, Дед Мороз {}!\n\nВаш получатель отметил на сайте, что подарок получен. ".format(request.member.santa.user.username) + 315 | "Это очень круто!") 316 | Season.objects.filter(year = request.season.year).update(delivered_count = F("delivered_count") + 1) 317 | gift_received.send(sender=Member, request=request) 318 | return _AjaxResponse({ 319 | "season": SeasonSerializer(request.season).data, 320 | "member": MemberSerializer(request.member).data, 321 | }) 322 | 323 | 324 | def jserror(request): 325 | if not request.body: 326 | return HttpResponse("No payload") 327 | logger.warning("JS: {ip} - {ua} - {payload}".format( 328 | ip=request.META["REMOTE_ADDR"], 329 | ua=request.META.get("HTTP_USER_AGENT", "-"), 330 | payload=json.loads(request.body))) 331 | return HttpResponse("OK") 332 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | 6 | if __name__ == '__main__': 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oldsanta.settings') 8 | 9 | from django.core.management import execute_from_command_line 10 | 11 | execute_from_command_line(sys.argv) 12 | -------------------------------------------------------------------------------- /oldsanta/__init__.py: -------------------------------------------------------------------------------- 1 | from oldsanta.celery import app as celery_app 2 | -------------------------------------------------------------------------------- /oldsanta/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.conf import settings 4 | 5 | from celery import Celery 6 | 7 | 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oldsanta.settings") 9 | 10 | 11 | app = Celery("oldsanta") 12 | 13 | app.config_from_object("django.conf:settings") 14 | app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) 15 | -------------------------------------------------------------------------------- /oldsanta/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | APPEND_SLASH = True 8 | 9 | DATABASES = { 10 | "default": { 11 | "ENGINE": "django.db.backends.sqlite3", 12 | "NAME": os.path.join(BASE_DIR, "oldsanta.sqlite3"), 13 | } 14 | } 15 | 16 | DEBUG = True 17 | 18 | INTERNAL_IPS = ( 19 | "127.0.0.1", 20 | ) 21 | 22 | INSTALLED_APPS = ( 23 | "django.contrib.admin", 24 | "django.contrib.auth", 25 | "django.contrib.contenttypes", 26 | "django.contrib.sessions", 27 | "django.contrib.messages", 28 | "django.contrib.staticfiles", 29 | "rest_framework", 30 | "pipeline", 31 | "clubadm", 32 | "actionlog", 33 | ) 34 | 35 | LANGUAGE_CODE = "ru-ru" 36 | 37 | LOGGING = { 38 | "version": 1, 39 | "disable_existing_loggers": False, 40 | "handlers": { 41 | "console": { 42 | "class": "logging.StreamHandler", 43 | }, 44 | }, 45 | "loggers": { 46 | "clubadm": { 47 | "handlers": ["console"], 48 | "level": "DEBUG", 49 | }, 50 | }, 51 | } 52 | 53 | MIDDLEWARE_CLASSES = ( 54 | "django.contrib.sessions.middleware.SessionMiddleware", 55 | "django.middleware.common.CommonMiddleware", 56 | "django.middleware.csrf.CsrfViewMiddleware", 57 | "django.contrib.auth.middleware.AuthenticationMiddleware", 58 | "django.contrib.auth.middleware.SessionAuthenticationMiddleware", 59 | "django.contrib.messages.middleware.MessageMiddleware", 60 | "django.middleware.security.SecurityMiddleware", 61 | "clubadm.middleware.SeasonMiddleware", 62 | "clubadm.middleware.MemberMiddleware", 63 | "clubadm.middleware.XUserMiddleware", 64 | ) 65 | 66 | ROOT_URLCONF = "oldsanta.urls" 67 | 68 | SECRET_KEY = "override this key in local_settings.py" 69 | 70 | TEMPLATES = [ 71 | { 72 | "BACKEND": "django.template.backends.django.DjangoTemplates", 73 | "DIRS": [ 74 | os.path.join(BASE_DIR, "oldsanta/templates"), 75 | ], 76 | "APP_DIRS": True, 77 | "OPTIONS": { 78 | "context_processors": [ 79 | "django.template.context_processors.debug", 80 | "django.template.context_processors.request", 81 | "django.contrib.auth.context_processors.auth", 82 | "django.contrib.messages.context_processors.messages", 83 | ], 84 | }, 85 | }, 86 | ] 87 | 88 | TIME_ZONE = "Europe/Moscow" 89 | 90 | USE_L10N = True 91 | 92 | USE_TZ = True 93 | 94 | WSGI_APPLICATION = "oldsanta.wsgi.application" 95 | 96 | 97 | AUTHENTICATION_BACKENDS = ( 98 | "clubadm.auth_backends.TechMediaBackend", 99 | ) 100 | 101 | AUTH_USER_MODEL = "clubadm.User" 102 | 103 | 104 | STATIC_ROOT = os.path.join(BASE_DIR, "oldsanta-static") 105 | 106 | STATIC_URL = "/static/" 107 | 108 | STATICFILES_DIRS = ( 109 | os.path.join(BASE_DIR, "node_modules", "angular"), 110 | os.path.join(BASE_DIR, "node_modules", "angular-i18n"), 111 | os.path.join(BASE_DIR, "node_modules", "moment"), 112 | os.path.join(BASE_DIR, "node_modules", "angular-moment"), 113 | os.path.join(BASE_DIR, "node_modules", "angularjs-scroll-glue"), 114 | os.path.join(BASE_DIR, "oldsanta", "static"), 115 | ) 116 | 117 | STATICFILES_STORAGE = "pipeline.storage.PipelineCachedStorage" 118 | 119 | STATICFILES_FINDERS = ( 120 | "django.contrib.staticfiles.finders.FileSystemFinder", 121 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 122 | "pipeline.finders.PipelineFinder", 123 | ) 124 | 125 | 126 | CELERY_ENABLE_UTC = True 127 | 128 | CELERY_TIMEZONE = TIME_ZONE 129 | 130 | CELERY_ACCEPT_CONTENT = ["json"] 131 | 132 | CELERY_TASK_SERIALIZER = "json" 133 | 134 | 135 | PIPELINE = { 136 | "STYLESHEETS": { 137 | "clubadm": { 138 | "source_filenames": ( 139 | "less/reset.less", 140 | "less/alert.less", 141 | "less/banner.less", 142 | "less/button.less", 143 | "less/card.less", 144 | "less/chat.less", 145 | "less/content.less", 146 | "less/counters.less", 147 | "less/feature.less", 148 | "less/footer.less", 149 | "less/header.less", 150 | "less/logo.less", 151 | "less/members.less", 152 | "less/profile.less", 153 | "less/promo.less", 154 | "less/scaffolding.less", 155 | "less/shipping.less", 156 | "less/timetable.less", 157 | "less/usercontrols.less", 158 | ), 159 | "output_filename": "clubadm.css", 160 | "variant": "datauri", 161 | }, 162 | }, 163 | "JAVASCRIPT": { 164 | "clubadm": { 165 | "source_filenames": ( 166 | "angular.min.js", 167 | "angular-locale_ru-ru.js", 168 | "min/moment.min.js", 169 | "locale/ru.js", 170 | "angular-moment.min.js", 171 | "src/scrollglue.js", 172 | "clubadm/clubadm.js", 173 | ), 174 | "output_filename": "clubadm.js", 175 | }, 176 | }, 177 | "COMPILERS": ( 178 | "pipeline.compilers.less.LessCompiler", 179 | ), 180 | "DISABLE_WRAPPER": True, 181 | "LESS_BINARY": os.path.join(BASE_DIR, "node_modules", ".bin", "lessc"), 182 | "YUGLIFY_BINARY": os.path.join(BASE_DIR, "node_modules", ".bin", "yuglify"), 183 | } 184 | 185 | 186 | TMAUTH_CLIENT = "12345" 187 | TMAUTH_SECRET = "santa" 188 | TMAUTH_TOKEN_URL = "http://localhost:5000/token" 189 | TMAUTH_LOGIN_URL = "http://localhost:5000/login" 190 | TMAUTH_ENDPOINT_URL = "http://localhost:5000" 191 | 192 | 193 | CLUBADM_ADMINS = ( 194 | # "negasus", 195 | "kafeman", 196 | # "mkruglova", 197 | ) 198 | 199 | CLUBADM_KARMA_LIMIT = 10.0 200 | 201 | 202 | try: 203 | from oldsanta.local_settings import * 204 | except ImportError: 205 | pass 206 | 207 | 208 | if DEBUG: 209 | INSTALLED_APPS += ( 210 | "debug_toolbar", 211 | ) 212 | MIDDLEWARE_CLASSES = ( 213 | "debug_toolbar.middleware.DebugToolbarMiddleware", 214 | ) + MIDDLEWARE_CLASSES 215 | -------------------------------------------------------------------------------- /oldsanta/static/images/anonymous.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/anonymous.png -------------------------------------------------------------------------------- /oldsanta/static/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/background.png -------------------------------------------------------------------------------- /oldsanta/static/images/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/buttons.png -------------------------------------------------------------------------------- /oldsanta/static/images/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/chat.png -------------------------------------------------------------------------------- /oldsanta/static/images/chat_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/chat_input.png -------------------------------------------------------------------------------- /oldsanta/static/images/checkbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/checkbox.png -------------------------------------------------------------------------------- /oldsanta/static/images/checkbox2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/checkbox2.png -------------------------------------------------------------------------------- /oldsanta/static/images/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/circle.png -------------------------------------------------------------------------------- /oldsanta/static/images/decorated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/decorated.png -------------------------------------------------------------------------------- /oldsanta/static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/favicon.ico -------------------------------------------------------------------------------- /oldsanta/static/images/features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/features.png -------------------------------------------------------------------------------- /oldsanta/static/images/gift_received.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/gift_received.jpg -------------------------------------------------------------------------------- /oldsanta/static/images/gift_sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/gift_sent.png -------------------------------------------------------------------------------- /oldsanta/static/images/gift_sent2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/gift_sent2.png -------------------------------------------------------------------------------- /oldsanta/static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/logo.png -------------------------------------------------------------------------------- /oldsanta/static/images/nothing_sent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/nothing_sent.png -------------------------------------------------------------------------------- /oldsanta/static/images/novosylov.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/novosylov.png -------------------------------------------------------------------------------- /oldsanta/static/images/stamp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/habrasanta/habrasanta-legacy/1e460253cdd30271aa359b53bdb600d2c6ca91b0/oldsanta/static/images/stamp.png -------------------------------------------------------------------------------- /oldsanta/static/less/alert.less: -------------------------------------------------------------------------------- 1 | .alert { 2 | background: #fc0; 3 | padding: 5px; 4 | text-align: center; 5 | 6 | a { 7 | color: #00c; 8 | } 9 | } 10 | 11 | .critical { 12 | background: #382751; 13 | bottom: 0; 14 | color: #fff; 15 | left: 0; 16 | padding: 50px 10px; 17 | position: absolute; 18 | right: 0; 19 | text-align: center; 20 | top: 0; 21 | } 22 | -------------------------------------------------------------------------------- /oldsanta/static/less/banner.less: -------------------------------------------------------------------------------- 1 | .banner { 2 | padding: 0 10px; 3 | margin: 0 auto; 4 | width: 940px; 5 | 6 | &-inner { 7 | border-bottom: 1px solid #4c3b65; 8 | padding-bottom: 50px; 9 | } 10 | 11 | &-members { 12 | float: right; 13 | margin-top: -30px; 14 | } 15 | 16 | &-logo { 17 | background: transparent url(../images/circle.png); 18 | height: 175px; 19 | margin: 30px auto; 20 | padding: 42px 27px; 21 | width: 205px; 22 | } 23 | 24 | &-welcome { 25 | color: #fff; 26 | font-size: 29px; 27 | margin-bottom: 30px; 28 | text-align: center; 29 | } 30 | 31 | &-button { 32 | display: block !important; 33 | font-size: 16px; 34 | margin: 0 auto; 35 | width: 280px; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /oldsanta/static/less/button.less: -------------------------------------------------------------------------------- 1 | .button { 2 | border-radius: 5px; 3 | border: none; 4 | box-shadow: 0px -2px 0px rgba(0, 0, 0, 0.2) inset; 5 | color: #fff; 6 | cursor: pointer; 7 | display: inline-block; 8 | padding: 10px; 9 | text-align: center; 10 | text-decoration: none; 11 | transition: background 0.3s; 12 | 13 | &::-moz-focus-inner { 14 | padding: 0; 15 | border: 0; 16 | } 17 | 18 | &:focus { 19 | outline: 0; 20 | } 21 | 22 | &-primary { 23 | background: #349dcc; 24 | 25 | &:hover { 26 | background: #4ad2ff; 27 | } 28 | 29 | &:active { 30 | background: #246e8f; 31 | box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.2) inset; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /oldsanta/static/less/card.less: -------------------------------------------------------------------------------- 1 | .card { 2 | -webkit-perspective: 800px; 3 | perspective: 800px; 4 | width: 380px; 5 | 6 | &-heading, 7 | &-front, 8 | &-back { 9 | background: #fff; 10 | color: #333; 11 | } 12 | 13 | &-heading { 14 | background-clip: padding-box; 15 | border-radius: 5px 5px 0px 0px; 16 | font-size: 20px; 17 | line-height: 60px; 18 | text-align: center; 19 | 20 | .chat-counter { 21 | float: right; 22 | background: #b40000; 23 | color: #fff; 24 | height: 16px; 25 | width: 16px; 26 | padding: 0; 27 | font-size: 10px; 28 | line-height: 15px; 29 | border-radius: 8px; 30 | margin-top: -17px; 31 | position: absolute; 32 | } 33 | } 34 | 35 | &-body { 36 | height: 380px; 37 | -webkit-transform-style: preserve-3d; 38 | transform-style: preserve-3d; 39 | -webkit-transition: -webkit-transform 1s; 40 | transition: transform 1s; 41 | width: 380px; 42 | } 43 | 44 | &-front, 45 | &-back { 46 | -webkit-backface-visibility: hidden; 47 | backface-visibility: hidden; 48 | border-radius: 0 0 5px 5px; 49 | box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15) inset; 50 | height: 330px; 51 | padding: 25px; 52 | position: absolute; 53 | width: 330px; 54 | } 55 | 56 | &-front { 57 | z-index: 0; 58 | } 59 | 60 | &-back { 61 | -ms-transform: rotateY(180deg); 62 | -webkit-transform: rotateY(180deg); 63 | transform: rotateY(180deg); 64 | } 65 | 66 | &-flipped &-body { 67 | -ms-transform: rotateY(180deg); 68 | -webkit-transform: rotateY(180deg); 69 | transform: rotateY(180deg); 70 | } 71 | 72 | &-santa { 73 | float: left; 74 | margin-left: 80px; 75 | } 76 | 77 | &-giftee { 78 | margin-left: 480px; 79 | } 80 | 81 | &-title { 82 | margin: 0px 42px; 83 | } 84 | 85 | &-avatar { 86 | border-radius: 5px; 87 | height: 40px; 88 | margin-right: 6px; 89 | margin-top: -3px; 90 | vertical-align: middle; 91 | width: 40px; 92 | } 93 | 94 | &-banned { 95 | a { 96 | color: #337ab7; 97 | text-decoration: none; 98 | 99 | &:hover { 100 | text-decoration: underline; 101 | } 102 | } 103 | 104 | p { 105 | font-size: 15px; 106 | margin-bottom: 15px; 107 | } 108 | } 109 | 110 | &-stamp { 111 | background: url(../images/stamp.png); 112 | color: #7f9eb1; 113 | float: right; 114 | font-size: 8px; 115 | height: 15px; 116 | margin-top: 7px; 117 | padding-left: 126px; 118 | padding-top: 41px; 119 | width: 25px; 120 | } 121 | 122 | &-decorated { 123 | background: #fff url(../images/decorated.png); 124 | } 125 | 126 | &-button { 127 | background: #754787; 128 | font-size: 15px; 129 | width: 100%; 130 | 131 | &:hover { 132 | background: #945aab; 133 | } 134 | 135 | &:active { 136 | background: #543360; 137 | } 138 | } 139 | 140 | &-nothing-sent { 141 | background: #3b9bcd; 142 | color: #fff; 143 | font-size: 22px; 144 | font-weight: bold; 145 | text-align: center; 146 | 147 | img { 148 | display: block; 149 | height: 138px; 150 | margin: 65px auto 40px auto; 151 | width: 138px; 152 | } 153 | } 154 | 155 | input[type=checkbox] { 156 | border: 0; 157 | clip: rect(0 0 0 0); 158 | height: 1px; 159 | margin: -1px; 160 | overflow: hidden; 161 | padding: 0; 162 | position: absolute; 163 | width: 1px; 164 | } 165 | 166 | input[type=checkbox] + label { 167 | background-image: url(../images/checkbox.png); 168 | background-position: 0 0; 169 | background-repeat: no-repeat; 170 | cursor: pointer; 171 | display: inline-block; 172 | font-size:15px; 173 | height: 17px; 174 | line-height: 17px; 175 | padding-left: 30px; 176 | vertical-align: middle; 177 | } 178 | 179 | input[type=checkbox]:checked + label { 180 | background-position: 0 -17px; 181 | } 182 | 183 | &-happy { 184 | background: #6da900 !important; 185 | color: #fff !important; 186 | text-align: center; 187 | 188 | h3 { 189 | font-size: 26px; 190 | margin-bottom: 260px; 191 | } 192 | 193 | a { 194 | color: #fff; 195 | } 196 | } 197 | 198 | &-closed { 199 | color: #999; 200 | padding: 13px 0; 201 | text-align: center; 202 | } 203 | 204 | &-flipper { 205 | background: transparent url(../images/chat.png); 206 | border: none; 207 | cursor: pointer; 208 | float: right; 209 | height: 17px; 210 | margin-right: 25px; 211 | margin-top: 23px; 212 | width: 17px; 213 | 214 | &::-moz-focus-inner { 215 | padding: 0; 216 | border: 0; 217 | } 218 | 219 | &:focus { 220 | outline: 0; 221 | } 222 | } 223 | 224 | &-gift-sent { 225 | background: #b40000 !important; 226 | color: #fff !important; 227 | font-size: 15px; 228 | text-align: center; 229 | 230 | img { 231 | width: 146px; 232 | height: 125px; 233 | margin: 20px 0; 234 | } 235 | 236 | h3 { 237 | font-weight: bold; 238 | font-size: 22px; 239 | margin-bottom: 10px; 240 | } 241 | } 242 | 243 | &-gift-received { 244 | background: url(../images/gift_received.jpg) !important; 245 | color: #fff !important; 246 | text-align: center; 247 | 248 | h3 { 249 | font-weight: bold; 250 | font-size: 22px; 251 | } 252 | } 253 | 254 | &-waiting { 255 | font-size: 16px; 256 | text-align: center; 257 | 258 | h3 { 259 | font-size: 21px; 260 | font-weight: bold; 261 | margin: 15px 0; 262 | } 263 | 264 | a { 265 | color: #fff; 266 | } 267 | } 268 | 269 | &-danger &-heading, 270 | &-danger &-front/*, 271 | &-danger &-back*/ { 272 | background: #b40000; 273 | color: #fff; 274 | } 275 | } 276 | 277 | label, 278 | input[type='text'], 279 | textarea { 280 | font: 16px/1.4 'PT Sans', Arial, sans-serif; 281 | box-sizing: border-box; 282 | resize: none; 283 | 284 | &:focus { 285 | outline: 0; 286 | } 287 | } 288 | 289 | input[type='text'], 290 | textarea { 291 | background: #fff; 292 | border: 1px solid #ccc; 293 | border-radius: 5px; 294 | padding: 7px 7px; 295 | margin: 5px 0; 296 | width: 100%; 297 | } 298 | 299 | input[type='text'].ng-invalid.ng-touched, 300 | textarea.ng-invalid.ng-touched { 301 | border-color: #a94442; 302 | color: #a94442; 303 | } 304 | 305 | label { 306 | display: block; 307 | } 308 | 309 | #fullname { 310 | margin-bottom: 15px; 311 | } 312 | 313 | #postcode { 314 | width: 150px; 315 | } 316 | 317 | #address { 318 | height: 120px; 319 | margin-bottom: 15px; 320 | } 321 | -------------------------------------------------------------------------------- /oldsanta/static/less/chat.less: -------------------------------------------------------------------------------- 1 | .chat { 2 | &-view { 3 | height: 273px; 4 | margin-bottom: 15px; 5 | overflow: auto; 6 | } 7 | 8 | &-message { 9 | color: #898788; 10 | font-size: 13px; 11 | margin: 15px 60px 15px 0; 12 | 13 | p { 14 | background: #b40000; 15 | border-radius: 5px; 16 | color: #fff; 17 | font-size: 15px; 18 | margin-bottom: 5px; 19 | padding: 15px; 20 | word-wrap: break-word; 21 | } 22 | 23 | &.is-author { 24 | margin: 15px 0 15px 60px; 25 | 26 | p { 27 | background: #3b9bcd; 28 | } 29 | } 30 | } 31 | 32 | &-last-visit { 33 | color: #898788; 34 | text-align: center; 35 | } 36 | 37 | &-input { 38 | background: url(../images/chat_input.png) 302px 9px no-repeat !important; 39 | padding-right: 35px !important; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /oldsanta/static/less/content.less: -------------------------------------------------------------------------------- 1 | .content { 2 | padding: 0 10px; 3 | margin: 70px auto; 4 | width: 940px; 5 | } 6 | -------------------------------------------------------------------------------- /oldsanta/static/less/counters.less: -------------------------------------------------------------------------------- 1 | .counters { 2 | height: 67px; 3 | margin-right: 160px; 4 | 5 | li { 6 | float: left; 7 | font-size: 17px; 8 | margin-right: 40px; 9 | text-align: center; 10 | } 11 | 12 | b { 13 | display: block; 14 | font-size: 20px; 15 | font-weight: bold; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /oldsanta/static/less/feature.less: -------------------------------------------------------------------------------- 1 | .feature { 2 | &:before { 3 | background-image: url(../images/features.png); 4 | content: ''; 5 | position: absolute; 6 | } 7 | 8 | &-description:before { 9 | height: 100px; 10 | margin-left: -134px; 11 | margin-top: 16px; 12 | width: 88px; 13 | } 14 | 15 | &-anonymity:before { 16 | background-position: -88px 0px; 17 | height: 118px; 18 | margin-left: -142px; 19 | margin-top: 6px; 20 | width: 105px; 21 | } 22 | 23 | &-gift:before { 24 | background-position: -193px 0px; 25 | height: 101px; 26 | margin-top: -5px; 27 | margin-left: -128px; 28 | width: 77px; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /oldsanta/static/less/footer.less: -------------------------------------------------------------------------------- 1 | .footer { 2 | padding: 0 10px; 3 | margin: 30px auto; 4 | width: 940px; 5 | 6 | &-designer { 7 | background: url(../images/novosylov.png) no-repeat 0px 10px; 8 | color: #fff; 9 | float: right; 10 | opacity: 0.5; 11 | padding-left: 29px; 12 | text-decoration: none; 13 | transition: opacity 0.3s ease-out; 14 | 15 | &:hover { 16 | opacity: 1.0; 17 | } 18 | } 19 | 20 | &-feedback { 21 | color: #857892; 22 | 23 | a { 24 | color: #9989ae; 25 | 26 | &:hover { 27 | color: #fff; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /oldsanta/static/less/header.less: -------------------------------------------------------------------------------- 1 | .header { 2 | color: #fff; 3 | height: 176px; 4 | padding: 0 10px; 5 | margin: 30px auto 0; 6 | width: 940px; 7 | 8 | &-logo { 9 | float: left; 10 | } 11 | 12 | &-inner { 13 | margin-left: 240px; 14 | padding: 20px 0; 15 | } 16 | 17 | &-usercontrols { 18 | float: right; 19 | margin-top: 5px; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /oldsanta/static/less/logo.less: -------------------------------------------------------------------------------- 1 | .logo { 2 | background: url(../images/logo.png); 3 | font-weight: bold; 4 | height: 71px; 5 | padding-top: 105px; 6 | text-align: center; 7 | width: 205px; 8 | 9 | h1 { 10 | color: #b40000; 11 | font-size: 20px; 12 | } 13 | 14 | h2 { 15 | color: #7ea1b5; 16 | font-size: 14px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /oldsanta/static/less/members.less: -------------------------------------------------------------------------------- 1 | .members { 2 | background: #911316; 3 | color: #fff; 4 | padding: 5px; 5 | text-align: center; 6 | width: 130px; 7 | } 8 | -------------------------------------------------------------------------------- /oldsanta/static/less/profile.less: -------------------------------------------------------------------------------- 1 | .profile { 2 | background: #382751 url(../images/background.png); 3 | 4 | .angular { 5 | opacity: 0; 6 | transition: opacity 500ms; 7 | } 8 | 9 | .angular.loaded { 10 | opacity: 1; 11 | } 12 | 13 | &-loading { 14 | background: rgba(0, 0, 0, 0.8); 15 | bottom: 0; 16 | color: #fff; 17 | font-size: 50px; 18 | left: 0; 19 | padding: 100px 0; 20 | position: fixed; 21 | right: 0; 22 | text-align: center; 23 | top: 0; 24 | z-index: 100; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /oldsanta/static/less/promo.less: -------------------------------------------------------------------------------- 1 | .promo { 2 | margin: 50px auto 100px; 3 | width: 620px; 4 | 5 | &-welcome { 6 | color: #fff; 7 | font-size: 27px; 8 | text-align: center; 9 | } 10 | 11 | &-list { 12 | margin-bottom: 100px; 13 | margin-top: 50px; 14 | } 15 | 16 | &-feature { 17 | color: #fff; 18 | margin-bottom: 50px; 19 | padding-left: 160px; 20 | 21 | h5 { 22 | font-weight: bold; 23 | margin-bottom: 10px; 24 | } 25 | } 26 | 27 | &-button { 28 | box-shadow: 0 0 0 10px #48306a, 0 -2px 0 rgba(0, 0, 0, 0.2) inset; 29 | display: block; 30 | font-size: 16px; 31 | margin: 10px auto; 32 | width: 280px; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /oldsanta/static/less/reset.less: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } 49 | -------------------------------------------------------------------------------- /oldsanta/static/less/scaffolding.less: -------------------------------------------------------------------------------- 1 | body { 2 | background: #382751; 3 | font: 14px/1.4 'PT Sans', Arial, sans-serif; 4 | } 5 | 6 | a { 7 | transition: color 0.3s ease-out; 8 | } 9 | -------------------------------------------------------------------------------- /oldsanta/static/less/shipping.less: -------------------------------------------------------------------------------- 1 | .shipping { 2 | &-confirmation { 3 | line-height: 41px; 4 | } 5 | 6 | &-button { 7 | background: #b40000; 8 | float: right; 9 | font-size: 15px; 10 | line-height: 21px; 11 | margin-right: 14px; 12 | transition: none; 13 | 14 | &:after { 15 | border-bottom: 19px solid transparent; 16 | border-left: 1em solid #b40000; 17 | border-top: 21px solid transparent; 18 | content: ""; 19 | margin-left: 8px; 20 | margin-top: -9px; 21 | position: absolute; 22 | } 23 | 24 | &:hover { 25 | background: #d30000; 26 | 27 | &:after { 28 | border-left: 1em solid #d30000; 29 | } 30 | } 31 | 32 | &:active { 33 | background: #990000; 34 | 35 | &:after { 36 | border-left: 1em solid #990000; 37 | } 38 | } 39 | } 40 | } 41 | 42 | .receiving { 43 | background: #3a9acc !important; 44 | color: #fff !important; 45 | text-align: center; 46 | font-size: 15px; 47 | 48 | img { 49 | width: 146px; 50 | height: 125px; 51 | margin: 20px 0; 52 | } 53 | 54 | h3 { 55 | font-weight: bold; 56 | font-size: 22px; 57 | margin-bottom: 10px; 58 | } 59 | 60 | &-confirmation { 61 | line-height: 41px; 62 | text-align: left; 63 | margin-top: 37px; 64 | } 65 | 66 | input[type=checkbox] + label { 67 | background-image: url(../images/checkbox2.png); 68 | } 69 | 70 | &-button { 71 | background: #f4f9ff; 72 | color: #333; 73 | float: right; 74 | font-size: 15px; 75 | line-height: 21px; 76 | margin-right: 14px; 77 | transition: none; 78 | 79 | &:after { 80 | border-bottom: 19px solid transparent; 81 | border-left: 1em solid #f4f9ff; 82 | border-top: 21px solid transparent; 83 | content: ""; 84 | margin-left: 8px; 85 | margin-top: -9px; 86 | position: absolute; 87 | } 88 | 89 | &:hover { 90 | background: #fff; 91 | 92 | &:after { 93 | border-left: 1em solid #fff; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /oldsanta/static/less/timetable.less: -------------------------------------------------------------------------------- 1 | .timetable { 2 | background: rgba(69, 48, 98, 0.8); 3 | border-radius: 5px; 4 | height: 65px; 5 | 6 | li { 7 | float: left; 8 | margin-right: 80px; 9 | padding: 13px 15px; 10 | } 11 | 12 | b { 13 | display: block; 14 | font-weight: bold; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /oldsanta/static/less/usercontrols.less: -------------------------------------------------------------------------------- 1 | .usercontrols { 2 | &-item { 3 | float: left; 4 | margin-left: 20px; 5 | 6 | a { 7 | background: url(../images/buttons.png); 8 | display: block; 9 | height: 44px; 10 | width: 44px; 11 | 12 | &:hover { 13 | background-position: 0 44px; 14 | } 15 | } 16 | } 17 | 18 | &-help a { 19 | background-position: 88px 0; 20 | 21 | &:hover { 22 | background-position: 88px 44px; 23 | } 24 | } 25 | 26 | &-logout a { 27 | background-position: 44px 0; 28 | 29 | &:hover { 30 | background-position: 44px 44px; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles pipeline %} 2 | 3 | 4 | 5 | 6 | Клуб анонимных Дедов Морозов {{ season.year }} на Хабрахабре 7 | 8 | {% stylesheet "clubadm" %} 9 | 10 | 11 | 12 | {% if season.is_closed %} 13 |
14 | Внимание! АДМ-{{ season.year }} уже завершен, вы смотрите архивную 15 | версию! Новая тут. 16 |
17 | {% endif %} 18 | {% block body %}{% endblock %} 19 | 28 | 49 | {% javascript "clubadm" %} 50 | 51 | 52 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/notifications/giftee_mail.html: -------------------------------------------------------------------------------- 1 | Ваш получатель подарка отправил вам сообщение. Прочитать его можно в профиле. 2 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/notifications/match.html: -------------------------------------------------------------------------------- 1 | Ура, ура! Вам был назначен получатель подарка. Посмотреть адрес можно в профиле. 2 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/notifications/santa_mail.html: -------------------------------------------------------------------------------- 1 | Дед Мороз отправил вам сообщение. Прочитать его можно в профиле. 2 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "clubadm/base.html" %} 2 | 3 | {% block body_attrs %} class="profile"{% endblock %} 4 | 5 | {% block body %} 6 | 11 |
14 | {% verbatim %} 15 | 80 |
81 |
84 |
85 | 93 |

94 | 95 | {{ user.username }} 96 |

97 |
98 |
99 |
101 |

С Новым Годом!

102 |

103 | Пост хвастовства подарками 104 |

105 |
106 |
108 | Подарок в пути 109 |

Вам отправили подарок

110 |

111 | Наберись терпения и не забывай проверять почту, твой 112 | подарок уже в пути. 113 |

114 |
115 | 120 | 122 | 123 |
124 |
125 |
127 | 128 | Вам пока ничего
не отправили 129 |
130 |
131 |
132 |
135 |

{{ msg.body }}

136 | 137 | Прочитано 138 | 139 | 140 | Отправлено 141 | 142 |
143 |
144 |
145 | Чатик закрыт {{ season.ship_by | date:'d MMMM yyyy' }} г. 146 |
147 |
148 | 151 |
152 |
153 |
154 |
155 |
158 |
159 | 160 | 162 |
{{ season.year }}
163 | 164 | 166 | 168 |
169 | 174 | 179 |
181 | Нужен полноправный аккаунт с кармой от +10 182 |
183 |
185 | Регистрация закрыта 186 | {{ season.signups_end | date:'d MMMM yyyy' }} г. 187 |
188 |
189 |
190 |

191 | Привет, {{ user.username }}. В прошлом году твой получатель 192 | не нажал кнопку подтверждения получения подарка. Скорее 193 | всего, он просто забыл об этом, но нам хотелось бы это 194 | выяснить. Напиши, пожалуйста, в ЛС хабрапользователю 195 | @negasus 196 | о том, что ты сейчас прочитал, и мы попробуем вместе 197 | разобраться. После этого ты сможешь заполнить свои данные 198 | и участвовать в ХабраАДМ. 199 |

200 |

201 | Извини, что так все сложно, но нам хочется, чтобы все было 202 | хорошо и правильно. 203 |

204 |

205 | С наступающим! 206 |

207 |
208 |
209 |
210 | 211 |
215 |
216 | 223 |

Ваш получатель

224 |
225 |
226 |
227 | Аноним 228 |

229 | 234 | до старта 235 |

236 |

237 | {{ season.signups_end | date:'d MMMM' }} будет проведена 238 | жеребьевка адресов, где каждому участнику будет назначен свой 239 | получатель. 240 |

241 |

242 | Адреса уже розданы 243 |

244 |

245 | Подписывайтесь на обновления 246 | @clubadm, чтобы 247 | не пропустить регистрацию на следующий год. 248 |

249 |
250 |
252 |

Подарок получен

253 |
254 |
256 | Подарок в пути 257 |

Вы отправили подарок

258 |

259 | Осталось дождаться пока получатель подтвердит получение подарка. 260 |

261 |
262 |
264 | 265 | 267 |
{{ season.year }}
268 | 269 | 271 | 273 |
274 | Вы так и не отправили подарок вовремя :-( 275 |
276 |
277 | 281 | 282 | 283 |
284 |
285 |
286 |
287 |
290 |

{{ msg.body }}

291 | 292 | Прочитано 293 | 294 | 295 | Отправлено 296 | 297 |
298 |
299 |
300 | Чатик закрыт {{ season.ship_by | date:'d MMMM yyyy' }} г. 301 |
302 |
303 | 306 |
307 |
308 |
309 |
310 |
311 | {% endverbatim %} 312 |
313 | 317 | {% endblock %} 318 | -------------------------------------------------------------------------------- /oldsanta/templates/clubadm/welcome.html: -------------------------------------------------------------------------------- 1 | {% extends "clubadm/base.html" %} 2 | 3 | {% block body %} 4 | 23 |
24 |

О клубе

25 |
    26 |
  • 27 |
    Итак, что же это такое?
    28 |

    29 | Хабраюзер регистрируется на нашем сайте в качестве участника 30 | акции, заполняет форму со своим адресом и ФИО, куда будет 31 | присылаться подарок. После окончания регистрации адреса случайным 32 | образом распределяются между участниками, и наступает время 33 | отправки подарков. 34 |

    35 |
  • 36 |
  • 37 |
    Анонимность
    38 |

    39 | Например, вам пришло сообщение, что вам необходимо отправить подарок 40 | Иванову Ивану по адресу: РФ, 101000, г. Москва, 41 | 3-я улица строителей, 25/12. Вы понятия не имеете, что за 42 | хабраюзер скрывается под этими данными. Точно так же ваш адрес попадет 43 | кому-то другому. Все здорово и анонимно. 44 |

    45 |
  • 46 |
  • 47 |
    Что посылать?
    48 |

    49 | Это, разумеется, на ваше усмотрение. Никаких ограничений тут нет. 50 | Просто при выборе подарка, прикиньте — хотелось бы вам получить 51 | что-то подобное? 52 |

    53 |
  • 54 |
55 | 57 | Войти через Хабр 58 | 59 |
60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /oldsanta/templates/pipeline/css.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oldsanta/templates/pipeline/js.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oldsanta/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import include, url 2 | 3 | 4 | urlpatterns = [ 5 | url(r'^', include('clubadm.urls')), 6 | ] 7 | -------------------------------------------------------------------------------- /oldsanta/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from django.core.wsgi import get_wsgi_application 4 | 5 | 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'oldsanta.settings') 7 | 8 | 9 | application = get_wsgi_application() 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "dependencies": { 4 | "angular": "1.6.0", 5 | "angular-i18n": "1.6.0", 6 | "angular-moment": "1.0.0-beta.3", 7 | "angularjs-scroll-glue": "2.0.7", 8 | "less": "3.10.3", 9 | "moment": "2.19.3", 10 | "yuglify": "2.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | celery==3.1.21 2 | django==1.11.23 3 | django-pipeline==1.6.8 4 | djangorestframework==3.9.1 5 | jinja2==2.10.1 6 | requests==2.20.0 7 | -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | celery==3.1.21 2 | django==1.11.23 3 | django-pipeline==1.6.8 4 | djangorestframework==3.9.1 5 | jinja2==2.10.1 6 | psycopg2==2.6.2 7 | pylibmc==1.5.1 8 | python-memcached==1.58 9 | requests==2.20.0 10 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | celery==3.1.21 2 | django==1.11.23 3 | django-debug-toolbar==1.5 4 | django-pipeline==1.6.8 5 | djangorestframework==3.9.1 6 | jinja2==2.10.1 7 | requests==2.20.0 8 | --------------------------------------------------------------------------------