├── .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 | [](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 |
7 |
8 | Один мальчик отключил JavaScript и не получил подарок.
9 |
10 |
11 |
14 | {% verbatim %}
15 |
80 |
81 |
84 |
98 |
99 |
106 |
108 |
109 |
Вам отправили подарок
110 |
111 | Наберись терпения и не забывай проверять почту, твой
112 | подарок уже в пути.
113 |
114 |
115 |
118 | Далее
119 |
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 |
152 |
153 |
154 |
155 |
158 |
169 |
172 | Я передумал участвовать
173 |
174 |
177 | Зарегистрировать участника
178 |
179 |
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 |
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 |
279 | Далее
280 |
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 |
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 |
--------------------------------------------------------------------------------