You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1093 lines
40 KiB
1093 lines
40 KiB
import logging |
|
logger = logging.getLogger(__name__) |
|
|
|
from django.contrib.auth.models import User, Group |
|
from django.shortcuts import get_object_or_404 |
|
from django.db.models import Max, F, Count, Q, Sum |
|
from django.utils.timezone import now |
|
from rest_framework import serializers |
|
from rest_framework.exceptions import ValidationError |
|
from rest_framework.validators import UniqueValidator |
|
from rest_auth.registration.serializers import RegisterSerializer |
|
from rest_auth.serializers import PasswordChangeSerializer, PasswordResetSerializer, PasswordResetConfirmSerializer, LoginSerializer |
|
from rest_auth.serializers import UserDetailsSerializer |
|
import re |
|
import datetime, time, calendar |
|
|
|
from . import models, fields, utils, utils_ldap, utils_auth, utils_stats |
|
from .. import settings, secrets |
|
from .permissions import is_admin_director |
|
|
|
class UsageSerializer(serializers.ModelSerializer): |
|
first_name = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.Usage |
|
fields = '__all__' |
|
|
|
def get_first_name(self, obj): |
|
return obj.user.member.preferred_name |
|
|
|
class ProtocoinTransactionSerializer(serializers.ModelSerializer): |
|
class Meta: |
|
model = models.Transaction |
|
fields = [ |
|
'id', |
|
'date', |
|
'protocoin', |
|
'account_type', |
|
'category', |
|
] |
|
|
|
class TransactionSerializer(serializers.ModelSerializer): |
|
# fields directly from old portal. replace with slugs we want |
|
account_type = serializers.ChoiceField([ |
|
'Interac', |
|
'TD Chequing', |
|
'Dream Pmt', |
|
'PayPal', |
|
'Square Pmt', |
|
'Member', |
|
'Clearing', |
|
'Cash', |
|
'Protocoin', |
|
]) |
|
category = serializers.ChoiceField([ |
|
'Membership', |
|
'OnAcct', |
|
'Snacks', |
|
'Donation', |
|
'Consumables', |
|
'Purchases', |
|
'Garage Sale', |
|
'Reimburse', |
|
'Other', |
|
'Exchange', |
|
]) |
|
member_id = serializers.SerializerMethodField() |
|
member_name = serializers.SerializerMethodField() |
|
date = serializers.DateField() |
|
report_type = serializers.ChoiceField([ |
|
'Unmatched Member', |
|
'Unmatched Purchase', |
|
'User Flagged', |
|
], allow_null=True, required=False) |
|
number_of_membership_months = serializers.IntegerField(max_value=36, min_value=-36, default=0) |
|
recorder = serializers.SerializerMethodField() |
|
amount = serializers.DecimalField(max_digits=7, decimal_places=2, default=0) |
|
protocoin = serializers.DecimalField(max_digits=7, decimal_places=2, default=0) |
|
|
|
class Meta: |
|
model = models.Transaction |
|
fields = '__all__' |
|
read_only_fields = [ |
|
'id', |
|
'user', |
|
'recorder', |
|
'paypal_txn_id', |
|
'paypal_txn_type', |
|
'paypal_payer_id', |
|
] |
|
|
|
def validate_transaction(self, validated_data): |
|
if not self.initial_data.get('member_id', None): |
|
raise ValidationError(dict(member_id='This field is required.')) |
|
|
|
member = get_object_or_404(models.Member, id=self.initial_data['member_id']) |
|
validated_data['user'] = member.user |
|
|
|
if validated_data['account_type'] == 'Protocoin': |
|
validated_data['amount'] = 0 |
|
else: |
|
validated_data['protocoin'] = 0 |
|
|
|
if validated_data['category'] != 'Membership': |
|
validated_data['number_of_membership_months'] = 0 |
|
|
|
if validated_data['category'] == 'Membership' and not validated_data['number_of_membership_months']: |
|
raise ValidationError(dict(number_of_membership_months='This field is required.')) |
|
|
|
if validated_data['account_type'] == 'Protocoin' and validated_data['category'] == 'Exchange': |
|
raise ValidationError(dict(category='Can\'t purchase Protocoin with Protocoin.')) |
|
|
|
if validated_data['category'] == 'Exchange': |
|
if validated_data['amount'] == 0: |
|
raise ValidationError(dict(category='Can\'t purchase 0 Protocoin.')) |
|
validated_data['protocoin'] = validated_data['amount'] |
|
|
|
if validated_data['account_type'] == 'Protocoin': |
|
if validated_data['protocoin'] == 0: |
|
raise ValidationError(dict(account_type='Can\'t have a 0.00 protocoin transaction.')) |
|
|
|
if validated_data['account_type'] not in ['Clearing', 'Protocoin']: |
|
if validated_data['amount'] == 0: |
|
raise ValidationError(dict(account_type='Can\'t have a $0.00 {} transaction. Do you want "Membership Adjustment"?'.format(validated_data['account_type']))) |
|
|
|
if validated_data['account_type'] in ['Interac', 'Dream Pmt', 'Square Pmt', 'PayPal']: |
|
if not validated_data.get('reference_number', None): |
|
raise ValidationError(dict(reference_number='This field is required.')) |
|
|
|
return validated_data |
|
|
|
def create(self, validated_data): |
|
validated_data = self.validate_transaction(validated_data) |
|
|
|
if validated_data['protocoin'] < 0: |
|
user = validated_data['user'] |
|
current_protocoin = user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] or 0 |
|
new_protocoin = current_protocoin + validated_data['protocoin'] |
|
if new_protocoin < 0: |
|
raise ValidationError(dict(category='Insufficient funds. Member only has {} protocoin.'.format(current_protocoin))) |
|
|
|
if validated_data['account_type'] == 'PayPal': |
|
msg = 'Manual PayPal transaction added:\n' + str(validated_data) |
|
utils.alert_tanner(msg) |
|
|
|
if validated_data['account_type'] == 'Protocoin': |
|
msg = 'Manual Protocoin transaction added:\n' + str(validated_data) |
|
utils.alert_tanner(msg) |
|
|
|
return super().create(validated_data) |
|
|
|
def update(self, instance, validated_data): |
|
validated_data = self.validate_transaction(validated_data) |
|
|
|
if validated_data['protocoin'] < 0: |
|
user = validated_data['user'] |
|
# when updating, we need to subtract out the transaction being edited |
|
current_protocoin = (user.transactions.aggregate(Sum('protocoin'))['protocoin__sum'] or 0) - instance.protocoin |
|
new_protocoin = current_protocoin + validated_data['protocoin'] |
|
if new_protocoin < 0: |
|
msg = 'Negative Protocoin transaction updated:\n' + str(validated_data) |
|
utils.alert_tanner(msg) |
|
|
|
return super().update(instance, validated_data) |
|
|
|
def get_member_id(self, obj): |
|
if not obj.user: return None |
|
return obj.user.member.id |
|
|
|
def get_member_name(self, obj): |
|
if not obj.user: return 'Unknown' |
|
|
|
member = obj.user.member |
|
return member.preferred_name + ' ' + member.last_name |
|
|
|
def get_recorder(self, obj): |
|
if obj.recorder: |
|
return obj.recorder.username |
|
else: |
|
return None |
|
|
|
|
|
# member viewing other members |
|
# hide info for non-vetted members so someone sitting |
|
# in our parking lot can't scrape all our info |
|
class OtherMemberSerializer(serializers.ModelSerializer): |
|
pinball_score = serializers.IntegerField(required=False) |
|
last_name = serializers.SerializerMethodField() |
|
storage = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.Member |
|
fields = [ |
|
'id', |
|
'preferred_name', |
|
'last_name', |
|
'status', |
|
'current_start_date', |
|
'application_date', |
|
'photo_small', |
|
'public_bio', |
|
'pinball_score', |
|
'storage', |
|
] |
|
|
|
def get_last_name(self, obj): |
|
if len(obj.last_name): |
|
return obj.last_name[0] + '.' |
|
else: |
|
return '' |
|
|
|
def get_storage(self, obj): |
|
serializer = SimpleStorageSpaceSerializer(data=obj.user.storage, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
# vetted member viewing other members |
|
class VettedOtherMemberSerializer(serializers.ModelSerializer): |
|
pinball_score = serializers.IntegerField(required=False) |
|
storage = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.Member |
|
fields = [ |
|
'id', |
|
'preferred_name', |
|
'last_name', |
|
'status', |
|
'current_start_date', |
|
'application_date', |
|
'photo_small', |
|
'photo_large', |
|
'public_bio', |
|
'pinball_score', |
|
'storage', |
|
] |
|
|
|
def get_storage(self, obj): |
|
serializer = StorageSpaceSerializer(data=obj.user.storage, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
|
|
# member viewing his own details |
|
class MemberSerializer(serializers.ModelSerializer): |
|
photo = serializers.ImageField(write_only=True, required=False) |
|
crop = serializers.CharField(write_only=True, required=False) |
|
email = fields.UserEmailField(serializers.EmailField) |
|
phone = serializers.CharField() |
|
protocoin = serializers.SerializerMethodField() |
|
sponsorship = OtherMemberSerializer(many=True, read_only=True) |
|
sponsored_by = OtherMemberSerializer(many=True, read_only=True) |
|
total_protocoin = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.Member |
|
fields = '__all__' |
|
read_only_fields = [ |
|
'id', |
|
'is_director', |
|
'is_staff', |
|
'is_instructor', |
|
'first_name', |
|
'last_name', |
|
'status', |
|
'expire_date', |
|
'current_start_date', |
|
'application_date', |
|
'vetted_date', |
|
'paused_date', |
|
'monthly_fees', |
|
'photo_large', |
|
'photo_medium', |
|
'photo_small', |
|
'member_forms', |
|
'card_photo', |
|
'user', |
|
'old_email', |
|
'orientation_date', |
|
'lathe_cert_date', |
|
'mill_cert_date', |
|
'wood_cert_date', |
|
'wood2_cert_date', |
|
'tormach_cnc_cert_date', |
|
'precix_cnc_cert_date', |
|
'rabbit_cert_date', |
|
'trotec_cert_date', |
|
'is_allowed_entry', |
|
'mediawiki_username', |
|
'signup_helper', |
|
] |
|
|
|
def get_protocoin(self, obj): |
|
transactions = obj.user.transactions |
|
total = transactions.aggregate(Sum('protocoin'))['protocoin__sum'] or 0 |
|
return total |
|
|
|
def get_total_protocoin(self, obj): |
|
transactions = models.Transaction.objects |
|
total = transactions.aggregate(Sum('protocoin'))['protocoin__sum'] or 0 |
|
return total |
|
|
|
def update(self, instance, validated_data): |
|
instance.user.email = validated_data.get('email', instance.user.email) |
|
instance.user.save() |
|
|
|
photo = validated_data.get('photo', None) |
|
crop = validated_data.get('crop', None) |
|
if photo: |
|
small, medium, large = utils.process_image_upload(photo, crop) |
|
instance.photo_small = small |
|
instance.photo_medium = medium |
|
instance.photo_large = large |
|
|
|
helper_id = self.initial_data.get('helper_id', None) |
|
if helper_id: |
|
signup_helper = get_object_or_404(models.Member, id=helper_id) |
|
instance.signup_helper = signup_helper.user |
|
|
|
is_student = self.initial_data.get('is_student', False) |
|
if is_student: |
|
instance.monthly_fees = 35 |
|
|
|
if 'discourse_username' in validated_data: |
|
changed = validated_data['discourse_username'] != instance.discourse_username |
|
if changed and utils_auth.discourse_is_configured(): |
|
username = instance.discourse_username |
|
new_username = validated_data['discourse_username'] |
|
logger.info('Changing discourse_username from %s to %s', username, new_username) |
|
if utils_auth.change_discourse_username(username, new_username) != 200: |
|
msg = 'Problem connecting to Discourse Auth server: change username.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(discourse_username='Invalid Discourse username.')) |
|
|
|
if validated_data.get('allow_last_scanned', None) == True: |
|
changed = validated_data['allow_last_scanned'] != instance.allow_last_scanned |
|
ONE_WEEK = now() - datetime.timedelta(days=7) |
|
if changed and models.HistoryChange.objects.filter( |
|
field='allow_last_scanned', |
|
index__history_user__member__id=instance.id, |
|
index__owner_id=instance.id, |
|
index__history_date__gte=ONE_WEEK, |
|
).count() >= 6: |
|
msg = 'Member allow_last_scanned rate limit exceeded by: ' + instance.preferred_name + ' ' + instance.last_name |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(allow_last_scanned='You\'re doing that too often.')) |
|
|
|
return super().update(instance, validated_data) |
|
|
|
# admin viewing member details |
|
class AdminMemberSerializer(MemberSerializer): |
|
phone = serializers.CharField(required=False) |
|
monthly_fees = serializers.ChoiceField([10, 30, 35, 50, 55]) |
|
|
|
class Meta: |
|
model = models.Member |
|
fields = '__all__' |
|
read_only_fields = [ |
|
'id', |
|
'status', |
|
'expire_date', |
|
'paused_date', |
|
'photo_large', |
|
'photo_medium', |
|
'photo_small', |
|
'member_forms', |
|
'card_photo', |
|
'user', |
|
'old_email', |
|
'is_director', |
|
'is_staff', |
|
'mediawiki_username', |
|
] |
|
|
|
def update(self, instance, validated_data): |
|
if 'is_allowed_entry' in validated_data: |
|
changed = validated_data['is_allowed_entry'] != instance.is_allowed_entry |
|
if changed: |
|
utils_stats.changed_card() |
|
|
|
if 'precix_cnc_cert_date' in validated_data: |
|
changed = validated_data['precix_cnc_cert_date'] != instance.precix_cnc_cert_date |
|
if changed: |
|
if validated_data['precix_cnc_cert_date']: |
|
utils_ldap.add_to_group(instance, 'CNC-Precix-Users') |
|
else: |
|
utils_ldap.remove_from_group(instance, 'CNC-Precix-Users') |
|
|
|
if 'rabbit_cert_date' in validated_data: |
|
changed = validated_data['rabbit_cert_date'] != instance.rabbit_cert_date |
|
if changed: |
|
if validated_data['rabbit_cert_date']: |
|
utils_ldap.add_to_group(instance, 'Laser Users') |
|
else: |
|
utils_ldap.remove_from_group(instance, 'Laser Users') |
|
|
|
if 'trotec_cert_date' in validated_data: |
|
changed = validated_data['trotec_cert_date'] != instance.trotec_cert_date |
|
if changed: |
|
if validated_data['trotec_cert_date']: |
|
utils_ldap.add_to_group(instance, 'Trotec Users') |
|
else: |
|
utils_ldap.remove_from_group(instance, 'Trotec Users') |
|
|
|
return super().update(instance, validated_data) |
|
|
|
|
|
# member viewing member list or search result |
|
class SearchSerializer(serializers.Serializer): |
|
q = serializers.CharField(write_only=True, max_length=64) |
|
seq = serializers.IntegerField(write_only=True) |
|
member = serializers.SerializerMethodField() |
|
|
|
def get_member(self, obj): |
|
serializer = OtherMemberSerializer(obj) |
|
return serializer.data |
|
|
|
# vetted member viewing member list or search result |
|
class VettedSearchSerializer(serializers.Serializer): |
|
q = serializers.CharField(write_only=True, max_length=64) |
|
seq = serializers.IntegerField(write_only=True) |
|
member = serializers.SerializerMethodField() |
|
|
|
def get_member(self, obj): |
|
serializer = VettedOtherMemberSerializer(obj) |
|
return serializer.data |
|
|
|
# instructor viewing search result |
|
class InstructorSearchSerializer(serializers.Serializer): |
|
member = serializers.SerializerMethodField() |
|
training = serializers.SerializerMethodField() |
|
|
|
def get_member(self, obj): |
|
serializer = VettedOtherMemberSerializer(obj) |
|
return serializer.data |
|
|
|
def get_training(self, obj): |
|
queryset = obj.user.training |
|
serializer = UserTrainingSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
# admin viewing search result |
|
class AdminSearchSerializer(serializers.Serializer): |
|
cards = serializers.SerializerMethodField() |
|
member = serializers.SerializerMethodField() |
|
training = serializers.SerializerMethodField() |
|
transactions = serializers.SerializerMethodField() |
|
#usages = serializers.SerializerMethodField() |
|
|
|
def get_member(self, obj): |
|
serializer = AdminMemberSerializer(obj) |
|
return serializer.data |
|
|
|
def get_cards(self, obj): |
|
queryset = obj.user.cards |
|
queryset = queryset.order_by('-last_seen') |
|
serializer = CardSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
def get_training(self, obj): |
|
queryset = obj.user.training |
|
serializer = UserTrainingSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
def get_transactions(self, obj): |
|
queryset = obj.user.transactions |
|
queryset = queryset.order_by('-date', '-id') |
|
serializer = TransactionSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
#def get_usages(self, obj): |
|
# queryset = obj.user.usages.order_by('-start_time') |
|
# serializer = UsageSerializer(data=queryset, many=True) |
|
# serializer.is_valid() |
|
# return serializer.data |
|
|
|
|
|
class CardSerializer(serializers.ModelSerializer): |
|
card_number = serializers.CharField(validators=[UniqueValidator( |
|
queryset=models.Card.objects.all(), |
|
message='Card number already exists.' |
|
)]) |
|
member_id = serializers.SerializerMethodField() |
|
active_status = serializers.ChoiceField([ |
|
'card_active', |
|
'card_inactive', |
|
]) |
|
|
|
class Meta: |
|
model = models.Card |
|
fields = '__all__' |
|
read_only_fields = [ |
|
'id', |
|
'last_seen', |
|
'last_seen_at', |
|
'user', |
|
] |
|
|
|
def create(self, validated_data): |
|
if not self.initial_data.get('member_id', None): |
|
raise ValidationError(dict(member_id='This field is required.')) |
|
|
|
member = get_object_or_404(models.Member, id=self.initial_data['member_id']) |
|
validated_data['user'] = member.user |
|
|
|
if not member.vetted_date: |
|
raise ValidationError(dict(non_field_errors='Member not vetted yet.')) |
|
return super().create(validated_data) |
|
|
|
def update(self, instance, validated_data): |
|
if not self.initial_data.get('member_id', None): |
|
raise ValidationError(dict(member_id='This field is required.')) |
|
|
|
member = get_object_or_404(models.Member, id=self.initial_data['member_id']) |
|
validated_data['user'] = member.user |
|
return super().update(instance, validated_data) |
|
|
|
def get_member_id(self, obj): |
|
if not obj.user: return None |
|
return obj.user.member.id |
|
|
|
|
|
class SimpleStorageSpaceSerializer(serializers.ModelSerializer): |
|
class Meta: |
|
model = models.StorageSpace |
|
fields = '__all__' |
|
|
|
|
|
class StorageSpaceSerializer(serializers.ModelSerializer): |
|
member_id = serializers.SerializerMethodField() |
|
member_name = serializers.SerializerMethodField() |
|
member_status = serializers.SerializerMethodField() |
|
member_paused = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.StorageSpace |
|
fields = '__all__' |
|
read_only_fields = [ |
|
'id', |
|
'shelf_id', |
|
'location', |
|
'user', |
|
] |
|
|
|
def update(self, instance, validated_data): |
|
member_id = self.initial_data.get('member_id', None) |
|
if member_id: |
|
member = get_object_or_404(models.Member, id=member_id) |
|
validated_data['user'] = member.user |
|
else: |
|
validated_data['user'] = None |
|
|
|
return super().update(instance, validated_data) |
|
|
|
def get_member_id(self, obj): |
|
if not obj.user: return None |
|
return obj.user.member.id |
|
|
|
def get_member_name(self, obj): |
|
if not obj.user: return None |
|
|
|
member = obj.user.member |
|
return member.preferred_name + ' ' + member.last_name |
|
|
|
def get_member_status(self, obj): |
|
if not obj.user: return None |
|
return obj.user.member.status |
|
|
|
def get_member_paused(self, obj): |
|
if not obj.user: return None |
|
return obj.user.member.paused_date |
|
|
|
|
|
class TrainingSerializer(serializers.ModelSerializer): |
|
attendance_status = serializers.ChoiceField([ |
|
'Waiting for payment', |
|
'Withdrawn', |
|
'Rescheduled', |
|
'Try-again', |
|
'No-show', |
|
'Attended', |
|
'Confirmed' |
|
]) |
|
session = serializers.PrimaryKeyRelatedField(queryset=models.Session.objects.all()) |
|
student_name = serializers.SerializerMethodField() |
|
student_email = serializers.SerializerMethodField() |
|
student_id = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.Training |
|
fields = '__all__' |
|
read_only_fields = ['user', 'sign_up_date', 'paid_date'] |
|
|
|
def get_student_name(self, obj): |
|
member = obj.user.member |
|
return member.preferred_name + ' ' + member.last_name |
|
|
|
def get_student_email(self, obj): |
|
return obj.user.email |
|
|
|
def get_student_id(self, obj): |
|
return obj.user.member.id |
|
|
|
def update(self, instance, validated_data): |
|
if validated_data['attendance_status'] == 'Waiting for payment' and instance.paid_date: |
|
validated_data['attendance_status'] = 'Confirmed' |
|
return super().update(instance, validated_data) |
|
|
|
|
|
class StudentTrainingSerializer(TrainingSerializer): |
|
attendance_status = serializers.ChoiceField(['Waiting for payment', 'Withdrawn']) |
|
|
|
|
|
class CourseSerializer(serializers.ModelSerializer): |
|
num_interested = serializers.IntegerField(read_only=True) |
|
|
|
class Meta: |
|
model = models.Course |
|
fields = ['id', 'name', 'is_old', 'description', 'tags', 'num_interested'] |
|
|
|
class SessionSerializer(serializers.ModelSerializer): |
|
student_count = serializers.SerializerMethodField() |
|
course_data = serializers.SerializerMethodField() |
|
instructor_name = serializers.SerializerMethodField() |
|
instructor_id = serializers.SerializerMethodField() |
|
datetime = serializers.DateTimeField() |
|
course = serializers.PrimaryKeyRelatedField(queryset=models.Course.objects.all()) |
|
students = TrainingSerializer(many=True, read_only=True) |
|
max_students = serializers.IntegerField(min_value=1, max_value=50, allow_null=True) |
|
cost = serializers.DecimalField(max_digits=None, decimal_places=2, min_value=0, max_value=200) |
|
|
|
class Meta: |
|
model = models.Session |
|
fields = '__all__' |
|
read_only_fields = ['old_instructor'] |
|
|
|
def get_student_count(self, obj): |
|
return len([x for x in obj.students.all() if x.attendance_status != 'Withdrawn']) |
|
|
|
def get_course_data(self, obj): |
|
return CourseSerializer(obj.course).data |
|
|
|
def get_instructor_name(self, obj): |
|
if obj.instructor and hasattr(obj.instructor, 'member'): |
|
name = '{} {}.'.format(obj.instructor.member.preferred_name, obj.instructor.member.last_name[0]) |
|
else: |
|
name = 'Unknown' |
|
return obj.old_instructor or name |
|
|
|
def get_instructor_id(self, obj): |
|
if obj.instructor and hasattr(obj.instructor, 'member'): |
|
return obj.instructor.member.id |
|
else: |
|
return None |
|
|
|
def create(self, validated_data): |
|
if validated_data['datetime'] < now() - datetime.timedelta(days=2): |
|
msg = 'Past class creation detected:\n' + str(validated_data) |
|
utils.alert_tanner(msg) |
|
raise ValidationError(dict(non_field_errors='Class can\'t be in the past.')) |
|
|
|
return super().create(validated_data) |
|
|
|
def update(self, instance, validated_data): |
|
if not self.initial_data.get('instructor_id', None): |
|
raise ValidationError(dict(instructor_id='This field is required.')) |
|
|
|
if validated_data['datetime'] < now() - datetime.timedelta(days=2): |
|
msg = 'Past class modification detected:\n' + str(validated_data) |
|
utils.alert_tanner(msg) |
|
raise ValidationError(dict(non_field_errors='Can\'t modify past class.')) |
|
|
|
member = get_object_or_404(models.Member, id=self.initial_data['instructor_id']) |
|
if not (is_admin_director(member.user) or member.is_instructor): |
|
raise ValidationError(dict(instructor_id='Member is not an instructor.')) |
|
|
|
validated_data['instructor'] = member.user |
|
return super().update(instance, validated_data) |
|
|
|
class SessionListSerializer(SessionSerializer): |
|
students = None |
|
|
|
|
|
class CourseDetailSerializer(serializers.ModelSerializer): |
|
sessions = SessionListSerializer(many=True, read_only=True) |
|
name = serializers.CharField(max_length=100) |
|
description = fields.HTMLField(max_length=6000) |
|
suggestion = serializers.SerializerMethodField() |
|
class Meta: |
|
model = models.Course |
|
fields = '__all__' |
|
|
|
def get_suggestion(self, obj): |
|
def iter_dates(): |
|
start_of_month = utils.today_alberta_tz().replace(day=1) |
|
for i in range(90): |
|
yield start_of_month + datetime.timedelta(days=i) |
|
|
|
def iter_matching_dates(weekday, week_num=False): |
|
week_num_counts = [0] * 13 |
|
for date in iter_dates(): |
|
if date.weekday() == weekday: |
|
week_num_counts[date.month] += 1 |
|
if week_num and week_num_counts[date.month] != week_num: |
|
continue |
|
yield date |
|
|
|
def next_date(weekday, week_num=False, fake_start=False): |
|
start = fake_start or utils.today_alberta_tz() |
|
for date in iter_matching_dates(weekday, week_num): |
|
if date > start: |
|
return date |
|
raise |
|
|
|
def course_is_usually_monthly(course): |
|
two_months_ago = utils.now_alberta_tz() - datetime.timedelta(days=61) |
|
recent_sessions = obj.sessions.filter(datetime__gte=two_months_ago) |
|
if recent_sessions.count() < 3: |
|
return True |
|
else: |
|
return False |
|
|
|
prev_session = obj.sessions.order_by('datetime').last() |
|
|
|
if obj.id == 273: # monthly clean 10:00 AM 3rd Saturday of each month |
|
date = next_date(calendar.SATURDAY, week_num=3) |
|
time = datetime.time(10, 0) |
|
dt = datetime.datetime.combine(date, time) |
|
dt = utils.TIMEZONE_CALGARY.localize(dt) |
|
cost = 0 |
|
max_students = None |
|
elif obj.id == 317: |
|
# members' meeting 7:00 PM 3rd Thursday of odd months, Wednesday of even months |
|
# but December's gets skipped |
|
next_month = next_date(calendar.WEDNESDAY, week_num=3).month |
|
if next_month == 12: |
|
one_month_ahead = utils.today_alberta_tz() + datetime.timedelta(days=31) |
|
date = next_date(calendar.THURSDAY, week_num=3, fake_start=one_month_ahead) |
|
elif next_month % 2 == 0: |
|
date = next_date(calendar.WEDNESDAY, week_num=3) |
|
else: |
|
date = next_date(calendar.THURSDAY, week_num=3) |
|
time = datetime.time(19, 0) |
|
dt = datetime.datetime.combine(date, time) |
|
dt = utils.TIMEZONE_CALGARY.localize(dt) |
|
cost = 0 |
|
max_students = None |
|
elif prev_session: |
|
dt = prev_session.datetime |
|
|
|
if course_is_usually_monthly(obj): |
|
offset_weeks = 4 |
|
else: |
|
offset_weeks = 1 |
|
dt = dt + datetime.timedelta(weeks=offset_weeks) |
|
|
|
five_days_from_now = utils.now_alberta_tz() + datetime.timedelta(days=5) |
|
while dt < five_days_from_now: |
|
dt = dt + datetime.timedelta(weeks=offset_weeks) |
|
|
|
cost = prev_session.cost |
|
max_students = prev_session.max_students |
|
else: |
|
return None |
|
|
|
return dict(datetime=dt, cost=str(cost), max_students=max_students) |
|
|
|
|
|
class UserTrainingSerializer(serializers.ModelSerializer): |
|
session = SessionListSerializer() |
|
class Meta: |
|
model = models.Training |
|
exclude = ['user'] |
|
depth = 2 |
|
|
|
class InterestSerializer(serializers.ModelSerializer): |
|
course = serializers.PrimaryKeyRelatedField(queryset=models.Course.objects.all()) |
|
class Meta: |
|
model = models.Interest |
|
fields = '__all__' |
|
read_only_fields = ['user', 'satisfied_by'] |
|
|
|
class UserSerializer(serializers.ModelSerializer): |
|
training = UserTrainingSerializer(many=True) |
|
member = MemberSerializer() |
|
transactions = serializers.SerializerMethodField() |
|
training = serializers.SerializerMethodField() |
|
interests = InterestSerializer(many=True) |
|
door_code = serializers.SerializerMethodField() |
|
wifi_pass = serializers.SerializerMethodField() |
|
app_version = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = User |
|
fields = [ |
|
'id', |
|
'username', |
|
'member', |
|
'transactions', |
|
'cards', |
|
'training', |
|
'is_staff', |
|
'door_code', |
|
'wifi_pass', |
|
'app_version', |
|
#'usages', |
|
'interests', |
|
'storage', |
|
] |
|
depth = 1 |
|
|
|
def get_transactions(self, obj): |
|
queryset = models.Transaction.objects.filter(user=obj) |
|
queryset = queryset.select_related('user', 'user__member') |
|
queryset = queryset.exclude(category='Memberships:Fake Months') |
|
queryset = queryset.order_by('-id', '-date') |
|
serializer = TransactionSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
def get_training(self, obj): |
|
queryset = obj.training |
|
queryset = queryset.select_related( |
|
'session', |
|
'session__course', |
|
'session__instructor', |
|
'session__instructor__member' |
|
) |
|
queryset = queryset.prefetch_related('session__students') |
|
queryset = queryset.order_by('-id') |
|
serializer = UserTrainingSerializer(data=queryset, many=True) |
|
serializer.is_valid() |
|
return serializer.data |
|
|
|
def get_door_code(self, obj): |
|
if not obj.member.paused_date and obj.cards.count(): |
|
return secrets.DOOR_CODE |
|
else: |
|
return None |
|
|
|
def get_wifi_pass(self, obj): |
|
if not obj.member.paused_date: |
|
return secrets.WIFI_PASS |
|
else: |
|
return None |
|
|
|
def get_app_version(self, obj): |
|
return settings.APP_VERSION |
|
|
|
|
|
class MyRegisterSerializer(RegisterSerializer): |
|
first_name = serializers.CharField(max_length=32) |
|
last_name = serializers.CharField(max_length=32) |
|
preferred_name = serializers.CharField(max_length=32) |
|
request_id = serializers.CharField(required=False) |
|
|
|
def validate_username(self, username): |
|
if re.search(r'[^a-z.]', username): |
|
raise ValidationError('Invalid characters.') |
|
if '..' in username: |
|
raise ValidationError('Can\'t have double periods. Remove spaces.') |
|
if username.startswith('.') or username.endswith('.'): |
|
raise ValidationError('Can\'t start or end with periods.') |
|
return super().validate_username(username) |
|
|
|
def custom_signup(self, request, user): |
|
data = request.data |
|
|
|
if not utils.is_request_from_protospace(request): |
|
logger.info('Request not from protospace') |
|
user.delete() |
|
raise ValidationError(dict(non_field_errors='Can only register from Protospace.')) |
|
|
|
if data['request_id']: utils_stats.set_progress(data['request_id'], 'Registering...') |
|
|
|
utils.register_user(data, user) |
|
|
|
class MyPasswordChangeSerializer(PasswordChangeSerializer): |
|
def save(self): |
|
request_id = self.request.data.get('request_id', '') |
|
|
|
data = dict( |
|
username=self.user.username, |
|
password1=self.request.data['new_password1'], |
|
) |
|
|
|
if utils_ldap.is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing LDAP password...') |
|
if utils_ldap.set_password(data) != 200: |
|
msg = 'Problem connecting to LDAP server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
|
|
data = dict( |
|
username=self.user.username, |
|
password=self.data['new_password1'], |
|
email=self.user.email, |
|
first_name=self.user.member.preferred_name, |
|
) |
|
|
|
data['username'] = self.user.member.mediawiki_username or self.user.username |
|
|
|
if utils_auth.wiki_is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing Wiki password...') |
|
if utils_auth.set_wiki_password(data) != 200: |
|
msg = 'Problem connecting to Wiki Auth server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
|
|
data['username'] = self.user.member.discourse_username or self.user.username |
|
|
|
if utils_auth.discourse_is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing Discourse password...') |
|
if utils_auth.set_discourse_password(data) != 200: |
|
msg = 'Problem connecting to Discourse Auth server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
if not self.user.member.discourse_username: |
|
self.user.member.discourse_username = self.user.username |
|
self.user.member.save() |
|
|
|
if request_id: utils_stats.set_progress(request_id, 'Changing Spaceport password...') |
|
time.sleep(1) |
|
|
|
super().save() |
|
|
|
class MyPasswordResetSerializer(PasswordResetSerializer): |
|
def validate_email(self, email): |
|
if not User.objects.filter(email__iexact=email).exists(): |
|
logging.info('Email not found: ' + email) |
|
raise ValidationError('Not found.') |
|
return super().validate_email(email) |
|
|
|
def save(self): |
|
email = self.data['email'] |
|
member = User.objects.get(email__iexact=email).member |
|
logging.info('Password reset requested for: {} - {} {} ({})'.format(email, member.preferred_name, member.last_name, member.id)) |
|
super().save() |
|
|
|
class MyPasswordResetConfirmSerializer(PasswordResetConfirmSerializer): |
|
def save(self): |
|
request_id = self.data['token'][-10:] |
|
|
|
data = dict( |
|
username=self.user.username, |
|
password1=self.data['new_password1'], |
|
) |
|
|
|
if utils_ldap.is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing LDAP password...') |
|
if utils_ldap.set_password(data) != 200: |
|
msg = 'Problem connecting to LDAP server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
|
|
data = dict( |
|
username=self.user.username, |
|
password=self.data['new_password1'], |
|
email=self.user.email, |
|
first_name=self.user.member.preferred_name, |
|
) |
|
|
|
data['username'] = self.user.member.mediawiki_username or self.user.username |
|
|
|
if utils_auth.wiki_is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing Wiki password...') |
|
if utils_auth.set_wiki_password(data) != 200: |
|
msg = 'Problem connecting to Wiki Auth server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
|
|
data['username'] = self.user.member.discourse_username or self.user.username |
|
|
|
if utils_auth.discourse_is_configured(): |
|
if request_id: utils_stats.set_progress(request_id, 'Changing Discourse password...') |
|
if utils_auth.set_discourse_password(data) != 200: |
|
msg = 'Problem connecting to Discourse Auth server: set.' |
|
utils.alert_tanner(msg) |
|
logger.info(msg) |
|
raise ValidationError(dict(non_field_errors=msg)) |
|
if not self.user.member.discourse_username: |
|
self.user.member.discourse_username = self.user.username |
|
self.user.member.save() |
|
|
|
member = self.user.member |
|
logging.info('Password reset completed for: {} {} ({})'.format(member.preferred_name, member.last_name, member.id)) |
|
|
|
if request_id: utils_stats.set_progress(request_id, 'Success! You can now log in as: ' + self.user.username) |
|
|
|
time.sleep(1) |
|
|
|
super().save() |
|
|
|
|
|
class MemberCountSerializer(serializers.ModelSerializer): |
|
class Meta: |
|
model = models.StatsMemberCount |
|
fields = '__all__' |
|
|
|
class SignupCountSerializer(serializers.ModelSerializer): |
|
month = serializers.SerializerMethodField() |
|
|
|
class Meta: |
|
model = models.StatsSignupCount |
|
fields = '__all__' |
|
|
|
def get_month(self, obj): |
|
return str(obj.month)[:7] |
|
|
|
class SpaceActivitySerializer(serializers.ModelSerializer): |
|
class Meta: |
|
model = models.StatsSpaceActivity |
|
fields = '__all__' |
|
|
|
|
|
class HistoryChangeSerializer(serializers.ModelSerializer): |
|
class Meta: |
|
model = models.HistoryChange |
|
fields = ['field', 'old', 'new'] |
|
|
|
class HistorySerializer(serializers.ModelSerializer): |
|
changes = HistoryChangeSerializer(many=True) |
|
history_user = serializers.StringRelatedField() |
|
|
|
class Meta: |
|
model = models.HistoryIndex |
|
fields = '__all__' |
|
|
|
class SpaceportAuthSerializer(LoginSerializer): |
|
def authenticate(self, **kwargs): |
|
user = super().authenticate(**kwargs) |
|
|
|
if user: |
|
data = self.context['request'].data.copy() |
|
data['email'] = user.email |
|
data['first_name'] = user.member.preferred_name |
|
|
|
data['username'] = user.member.mediawiki_username or user.username |
|
utils_auth.set_wiki_password(data) |
|
|
|
data['username'] = user.member.discourse_username or user.username |
|
utils_auth.set_discourse_password(data) |
|
|
|
if not user.member.paused_date: |
|
utils_auth.add_discourse_group_members('protospace_members', [data['username']]) |
|
|
|
if not user.member.discourse_username: |
|
user.member.discourse_username = user.username |
|
user.member.save() |
|
|
|
return user |
|
|
|
class MyLoginSerializer(LoginSerializer): |
|
def authenticate(self, **kwargs): |
|
username = kwargs.get('username', '') |
|
|
|
if 'your' in username and 'own' in username and 'name' in username: |
|
raise ValidationError(dict(username='*server explodes*')) |
|
|
|
if ' ' in username: |
|
raise ValidationError(dict(username='Username shouldn\'t have spaces. Try "first.last" or "first.middle.last".')) |
|
|
|
if 'first.last' in username: |
|
raise ValidationError(dict(username='Don\'t literally try "first.last", use your own name.')) |
|
|
|
if 'first.middle.last' in username: |
|
raise ValidationError(dict(username='Don\'t literally try "first.middle.last", use your own name.')) |
|
|
|
if not User.objects.filter(username=username).exists(): |
|
raise ValidationError(dict(username='Username not found. Try "first.last" or "first.middle.last".')) |
|
|
|
try: |
|
_ = User.objects.get(username=username).member |
|
except User.member.RelatedObjectDoesNotExist: |
|
raise ValidationError(dict(username='Can\'t log in as superuser. Make an account below.')) |
|
|
|
user = super().authenticate(**kwargs) |
|
|
|
if not user: |
|
raise ValidationError(dict(password='Incorrect password. Check caps lock.')) |
|
|
|
return user
|
|
|