# pylint: disable=too-many-lines; Allow more than 1000 linesimportloggingfrombase64importb64encodefromdatetimeimporttimedeltafromtypingimportCollection,Dict,Optional,Unionfromdateutil.relativedeltaimportrelativedeltafromdjango.confimportsettingsfromdjango.contrib.auth.modelsimportAbstractBaseUser,AnonymousUser,PermissionsMixinfromdjango.contrib.auth.modelsimportUserManagerasDjangoUserManagerfromdjango.core.cacheimportcachefromdjango.dbimportmodelsfromdjango.db.modelsimportCount,F,Q,QuerySetfromdjango.utilsimporttimezonefromdjango.utils.functionalimportcached_propertyfromintranet.middlewareimportthreadlocalsfrom...utils.dateimportget_senior_graduation_yearfrom...utils.helpersimportis_entirely_digitfrom..bus.modelsimportRoutefrom..eighth.modelsimportEighthBlock,EighthSignup,EighthSponsorfrom..groups.modelsimportGroupfrom..polls.modelsimportPollfrom..preferences.fieldsimportPhoneFieldlogger=logging.getLogger(__name__)# TODO: this is disgustingGRADE_NUMBERS=((9,"freshman"),(10,"sophomore"),(11,"junior"),(12,"senior"),(13,"staff"))# Eighth Office/Demo Student user IDs that should be excluded from teacher/student listsEXTRA=[9996,8888,7011]
[docs]classUserManager(DjangoUserManager):"""User model Manager for table-level User queries. Provides an abstraction for the User model. If a call to a method fails for this Manager, the call is deferred to the default User model manager. """
[docs]defuser_with_student_id(self,student_id:Union[int,str])->Optional["User"]:"""Get a unique user object by FCPS student ID. (Ex. 1624472)"""results=User.objects.filter(student_id=str(student_id))iflen(results)==1:returnresults.first()returnNone
[docs]defuser_with_ion_id(self,student_id:Union[int,str])->Optional["User"]:"""Get a unique user object by Ion ID. (Ex. 489)"""ifisinstance(student_id,str)andnotis_entirely_digit(student_id):returnNoneresults=User.objects.filter(id=str(student_id))iflen(results)==1:returnresults.first()returnNone
[docs]defusers_in_year(self,year:int)->Union[Collection["User"],QuerySet]:# pylint: disable=unsubscriptable-object"""Get a list of users in a specific graduation year."""returnUser.objects.filter(graduation_year=year)
[docs]defuser_with_name(self,given_name:Optional[str]=None,last_name:Optional[str]=None)->"User":# pylint: disable=unsubscriptable-object"""Get a unique user object by given name (first/nickname) and/or last name. Args: given_name: If given, users will be filtered to those who have either this first name or this nickname. last_name: If given, users will be filtered to those who have this last name. Returns: The unique user object returned by filtering for the given first name/nickname and/or last name. Returns ``None`` if no results were returned or if the given parameters matched more than one user. """results=User.objects.all()iflast_name:results=results.filter(last_name=last_name)ifgiven_name:results=results.filter(Q(first_name=given_name)|Q(nickname=given_name))try:returnresults.get()except(User.DoesNotExist,User.MultipleObjectsReturned):returnNone
[docs]defget_students(self)->Union[Collection["User"],QuerySet]:# pylint: disable=unsubscriptable-object"""Get user objects that are students (quickly)."""users=User.objects.filter(user_type="student",graduation_year__gte=get_senior_graduation_year())users=users.exclude(id__in=EXTRA)returnusers
[docs]defget_teachers(self)->Union[Collection["User"],QuerySet]:# pylint: disable=unsubscriptable-object"""Get user objects that are teachers (quickly)."""users=User.objects.filter(user_type="teacher")users=users.exclude(id__in=EXTRA)# Add possible exceptions handling hereusers=users|User.objects.filter(id__in=[31863,32327,32103,33228])users=users.exclude(Q(first_name=None)|Q(first_name="")|Q(last_name=None)|Q(last_name=""))returnusers
[docs]defget_teachers_attendance_users(self)->"QuerySet[User]":"""Like ``get_teachers()``, but includes attendance-only users as well as teachers. Returns: A QuerySet of users who are either teachers or attendance-only users. """users=User.objects.filter(user_type__in=["teacher","user"])users=users.exclude(id__in=EXTRA)# Add possible exceptions handling hereusers=users|User.objects.filter(id__in=[31863,32327,32103,33228])users=users.exclude(Q(first_name=None)|Q(first_name="")|Q(last_name=None)|Q(last_name=""))returnusers
[docs]defget_teachers_sorted(self)->Union[Collection["User"],QuerySet]:# pylint: disable=unsubscriptable-object"""Returns a ``QuerySet`` of teachers sorted by last name, then first name. Returns: A ``QuerySet`` of teachers sorted by last name, then first name. """returnself.get_teachers().order_by("last_name","first_name")
[docs]defget_teachers_attendance_users_sorted(self)->"QuerySet[User]":"""Returns a ``QuerySet`` containing both teachers and attendance-only users sorted by last name, then first name. Returns: A ``QuerySet`` of teachers sorted by last name, then first name. """returnself.get_teachers_attendance_users().order_by("last_name","first_name")
[docs]defget_approve_announcements_users(self)->"QuerySet[User]":"""Returns a ``QuerySet`` containing all users except simple users, tjstar presenters, alumni, service users and students. Returns: A ``QuerySet`` of all users except simple users, tjstar presenters, alumni, service users and students. """users=User.objects.filter(user_type__in=["user","teacher","counselor"])users=users.exclude(id__in=EXTRA)users=users.exclude(Q(first_name=None)|Q(first_name="")|Q(last_name=None)|Q(last_name=""))returnusers
[docs]defget_approve_announcements_users_sorted(self)->"QuerySet[User]":"""Returns a ``QuerySet`` containing all users except simple users, tjstar presenters, alumni, service users and students sorted by last name, then first name. This is used for the announcement request page. Returns: A ``QuerySet`` of all users except simple users, tjstar presenters, alumni, service users and students sorted by last name, then first name. """returnself.get_approve_announcements_users().order_by("last_name","first_name")
[docs]classUser(AbstractBaseUser,PermissionsMixin):"""Django User model subclass"""TITLES=(("Mr.","Mr."),("Ms.","Ms."),("Mrs.","Mrs."),("Dr.","Dr."),("Mx.","Mx."))USER_TYPES=(("student","Student"),("teacher","Teacher"),("counselor","Counselor"),("user","Attendance-Only User"),("simple_user","Simple User"),("tjstar_presenter","tjStar Presenter"),("alum","Alumnus"),("service","Service Account"),)GENDER=(("male","Male"),("female","Female"),("non-binary","Non-Binary"),)TITLES=(("mr","Mr."),("ms","Ms."),("mrs","Mrs."),("dr","Dr."),("mx","Mx."))# Django Model Fieldsusername=models.CharField(max_length=30,unique=True)# See Email model for emails# See Phone model for phone numbers# See Website model for websitesuser_locked=models.BooleanField(default=False)oauth_and_api_access=models.BooleanField(default=True,help_text="Whether the user can create OAuth applications and access the API.")# Local internal fieldsfirst_login=models.DateTimeField(null=True,blank=True)seen_welcome=models.BooleanField(default=False)last_global_logout_time=models.DateTimeField(null=True,blank=True)# Local preference fieldsreceive_news_emails=models.BooleanField(default=True)receive_eighth_emails=models.BooleanField(default=True)receive_schedule_notifications=models.BooleanField(default=False)student_id=models.CharField(max_length=settings.FCPS_STUDENT_ID_LENGTH,unique=True,null=True,blank=True)user_type=models.CharField(max_length=30,choices=USER_TYPES,default="student")admin_comments=models.TextField(blank=True,null=True)counselor=models.ForeignKey("self",on_delete=models.SET_NULL,related_name="students",null=True,blank=True)graduation_year=models.IntegerField(null=True,blank=True)title=models.CharField(max_length=5,choices=TITLES,null=True,blank=True)first_name=models.CharField(max_length=35,null=True)middle_name=models.CharField(max_length=70,null=True,blank=True)last_name=models.CharField(max_length=70,null=True)nickname=models.CharField(max_length=35,null=True,blank=True)gender=models.CharField(max_length=35,choices=GENDER,null=True,blank=True)preferred_photo=models.OneToOneField("Photo",related_name="+",null=True,blank=True,on_delete=models.SET_NULL)primary_email=models.OneToOneField("Email",related_name="+",null=True,blank=True,on_delete=models.SET_NULL)bus_route=models.ForeignKey(Route,on_delete=models.SET_NULL,null=True,blank=True)# April Fools 2023seen_april_fools=models.BooleanField(default=False)enable_april_fools=models.BooleanField(default=False)# Required to replace the default Django User modelUSERNAME_FIELD="username""""Override default Model Manager (objects) with custom UserManager to add table-level functionality."""objects=UserManager()
[docs]@staticmethoddefget_signage_user()->"User":"""Returns the user used to authenticate signage displays Returns: The user used to authenticate signage displays """returnUser(id=99999)
@propertydefaddress(self)->Optional["Address"]:"""Returns the ``Address`` object representing this user's address, or ``None`` if it is not set or the current user does not have permission to access it. Returns: The ``Address`` representing this user's address, or ``None`` if that is unavailable to the current user. """returnself.properties.address@propertydefschedule(self)->Optional[Union[QuerySet,Collection["Section"]]]:# pylint: disable=unsubscriptable-object"""Returns a QuerySet of the ``Section`` objects representing the classes this student is in, or ``None`` if the current user does not have permission to list this student's classes. Returns: Returns a QuerySet of the ``Section`` objects representing the classes this student is in, or ``None`` if the current user does not have permission to list this student's classes. """returnself.properties.schedule
[docs]defmember_of(self,group:Union[Group,str])->bool:"""Returns whether a user is a member of a certain group. Args: group: Either the name of a group or a ``Group`` object. Returns: Whether the user is a member of the given group. """ifisinstance(group,Group):group=group.namereturnself.groups.filter(name=group).cache(ops=["exists"],timeout=15).exists()# pylint: disable=no-member
[docs]defhas_admin_permission(self,perm:str)->bool:"""Returns whether a user has an admin permission (explicitly, or implied by being in the "admin_all" group) Args: perm: The admin permission to check for. Returns: Whether the user has the given admin permission (either explicitly or implicitly) """returnself.member_of("admin_all")orself.member_of("admin_"+perm)
@propertydeffull_name(self)->str:"""Return full name, e.g. Angela William. This is required for subclasses of User. Returns: The user's full name (first + " " + last). """returnf"{self.first_name}{self.last_name}"@propertydeffull_name_nick(self)->str:"""If the user has a nickname, returns their name in the format "Nickname Lastname." Otherwise, this is identical to full_name. Returns: The user's full name, with their nickname substituted for their first name if it is set. """returnf"{self.nicknameorself.first_name}{self.last_name}"@propertydefdisplay_name(self)->str:"""Returns ``self.full_name``. Returns: The user's full name. """returnself.full_name@propertydeflast_first(self)->str:"""Return a name in the format of: Lastname, Firstname [(Nickname)] """returnf"{self.last_name}, {self.first_name}"+(f" ({self.nickname})"ifself.nicknameelse"")@propertydeflast_first_id(self)->str:"""Return a name in the format of: Lastname, Firstname [(Nickname)] (Student ID/ID/Username) """return("{}{} ".format(self.last_name,", "+self.first_nameifself.first_nameelse"")+(f"({self.nickname}) "ifself.nicknameelse"")+(f"({self.student_idifself.is_studentandself.student_idelseself.username})"))@propertydeflast_first_initial(self)->str:"""Return a name in the format of: Lastname, F [(Nickname)] """return"{}{}".format(self.last_name,", "+self.first_name[:1]+"."ifself.first_nameelse"")+(f" ({self.nickname})"ifself.nicknameelse"")@propertydefshort_name(self)->str:"""Return short name (first name) of a user. This is required for subclasses of User. Returns: The user's fist name. """returnself.first_name
[docs]defget_full_name(self)->str:"""Return full name, e.g. Angela William. Returns: The user's full name (see ``full_name``). """returnself.full_name
[docs]defget_short_name(self)->str:"""Get short (first) name of a user. Returns: The user's first name (see ``short_name`` and ``first_name``). """returnself.short_name
@propertydefprimary_email_address(self)->Optional[str]:try:returnself.primary_email.addressifself.primary_emailelseNoneexceptEmail.DoesNotExist:returnNone@propertydeftj_email(self)->str:"""Get (or guess) a user's TJ email. If a fcps.edu or tjhsst.edu email is specified in their email list, use that. Otherwise, append the user's username to the proper email suffix, depending on whether they are a student or teacher. Returns: The user's found or guessed FCPS/TJ email address. """email=self.emails.filter(Q(address__iendswith="@fcps.edu")|Q(address__iendswith="@tjhsst.edu")).first()ifemailisnotNone:returnemail.addressifself.is_teacher:domain="fcps.edu"else:domain="tjhsst.edu"returnf"{self.username}@{domain}"@propertydefnon_tj_email(self)->Optional[str]:""" Returns the user's first non-TJ email found, or None if none is found. If a user has a primary email set and it is not their TJ email, use that. Otherwise, use the first email found that is not their TJ email. Returns: The first non-TJ email found for a user, or None if no such email is found. """tj_email=self.tj_emailprimary_email_address=self.primary_email_addressifprimary_email_addressandprimary_email_address.lower()!=tj_email.lower():returnprimary_email_addressemail=self.emails.exclude(address__iexact=tj_email).first()returnemail.addressifemailelseNone@propertydefnotification_email(self)->str:"""Returns the notification email. If a primary email is set, use it. Otherwise, use the first email on file. If no email addresses exist, use the user's TJ email. Returns: A user's notification email address. """primary_email_address=self.primary_email_addressifprimary_email_address:returnprimary_email_addressemail=self.emails.first()returnemail.addressifemailandemail.addresselseself.tj_email@propertydefdefault_photo(self)->Optional[bytes]:"""Returns default photo (in binary) that should be used Returns: The binary representation of the user's default photo. """preferred=self.preferred_photoifpreferredisnotNone:returnpreferred.binaryifself.user_type=="teacher":current_grade=13else:current_grade=min(int(self.grade),12)foriinreversed(range(9,current_grade+1)):data=Noneifself.photos.filter(grade_number=i).exists():data=self.photos.filter(grade_number=i).last().binaryifdata:returndatareturnNone@propertydefgrade(self)->"Grade":"""Returns the grade of a user. Returns: A Grade object representing the user's current grade. """returnGrade(self.graduation_year)@propertydefpermissions(self)->Dict[str,bool]:"""Dynamically generate dictionary of privacy options. Returns: A dictionary mapping the name of each privacy option to a boolean indicating whether it is enabled. """permissions_dict={}forprefixinPERMISSIONS_NAMES:permissions_dict[prefix]={}forsuffixinPERMISSIONS_NAMES[prefix]:permissions_dict[prefix][suffix]=getattr(self.properties,prefix+"_"+suffix)returnpermissions_dict
[docs]def_current_user_override(self)->bool:"""Return whether the currently logged in user is a teacher or eighth admin, and can view all of a student's information regardless of their privacy settings. Returns: Whether the user has permissions to view all of their information regardless of their privacy settings. """try:# threadlocals is a module, not an actual thread locals objectrequest=threadlocals.request()ifrequestisNone:returnFalserequesting_user=request.userifisinstance(requesting_user,AnonymousUser)ornotrequesting_user.is_authenticated:returnFalsecan_view_anyway=requesting_userand(requesting_user.is_teacherorrequesting_user.is_eighthofficeorrequesting_user.is_eighth_admin)except(AttributeError,KeyError)ase:logger.error("Could not check teacher/eighth override: %s",e)can_view_anyway=Falsereturncan_view_anyway
@propertydefion_username(self)->str:"""Returns this user's username. Returns: This user's username (see ``username``). """returnself.username@propertydefgrade_number(self)->int:"""Returns the number of the grade this user is currently in (9, 10, 11, or 12 for students). Returns: The number of the grade this user is currently in. """returnself.grade.number@propertydefsex(self)->str:"""Returns the gender of this user (male, female, or non-binary). Returns: The gender of this user (male, female, or non-binary). """returnself.genderor""@propertydefis_male(self)->bool:"""Returns whether the user is male. Returns: Whether this user is male. """returnself.gender=="male"@propertydefis_female(self)->bool:"""Returns whether the user is female. Returns: Whether this user is female. """returnself.gender=="female"@propertydefis_nonbinary(self)->bool:"""Returns whether the user is non-binary. Returns: Whether this user is non-binary. """returnself.gender=="non-binary"@propertydefcan_view_eighth(self)->bool:"""Checks if a user has the show_eighth permission. Returns: Whether this user has made their eighth period signups public. """returnself.properties.attribute_is_visible("show_eighth")@propertydefcan_view_phone(self)->bool:"""Checks if a user has the show_telephone permission. Returns: Whether this user has made their phone number public. """returnself.properties.attribute_is_visible("show_telephone")@propertydefis_eighth_admin(self)->bool:"""Checks if user is an eighth period admin. Returns: Whether this user is an eighth period admin. """returnself.has_admin_permission("eighth")@propertydefis_printing_admin(self)->bool:"""Checks if user has the admin permission 'printing'. Returns: Whether this user is a printing administrator. """returnself.has_admin_permission("printing")@propertydefis_parking_admin(self)->bool:"""Checks if user has the admin permission 'parking'. Returns: Whether this user is a parking administrator. """returnself.has_admin_permission("parking")@propertydefis_bus_admin(self)->bool:"""Returns whether the user has the ``bus`` admin permission. Returns: Whether the user has the ``bus`` admin permission. """returnself.has_admin_permission("bus")@propertydefcan_request_parking(self)->bool:"""Checks if user can view the parking interface. Returns: Whether this user can view the parking interface and request a parking spot. """returnself.grade_number>=11orself.is_parking_admin@propertydefis_announcements_admin(self)->bool:"""Checks if user is an announcements admin. Returns: Whether this user is an announcement admin. """returnself.has_admin_permission("announcements")@propertydefis_events_admin(self)->bool:"""Checks if user is an events admin. Returns: Whether this user is an events admin. """returnself.has_admin_permission("events")@propertydefis_schedule_admin(self)->bool:"""Checks if user is a schedule admin. Returns: Whether this user is a schedule admin. """returnself.has_admin_permission("schedule")@propertydefis_enrichment_admin(self)->bool:"""Checks if user is an enrichment admin. Returns: Whether this user is an enrichment admin. """returnself.has_admin_permission("enrichment")@propertydefis_board_admin(self)->bool:"""Checks if user is a board admin. Returns: Whether this user is a board admin. """returnself.has_admin_permission("board")@propertydefis_global_admin(self)->bool:"""Checks if user is a global admin. Returns: Whether this user is a global admin, defined as having admin_all, Django staff, and Django superuser. """returnself.member_of("admin_all")andself.is_staffandself.is_superuser
[docs]defcan_manage_group(self,group:Union[Group,str])->bool:"""Checks whether this user has permission to edit/manage the given group (either a Group or a group name). WARNING: Granting permission to edit/manage "admin_" groups gives that user control over nearly all data on Ion! Args: group: The group to check permissions for. Returns: Whether this user has permission to edit/manage the given group. """ifisinstance(group,Group):group=group.nameifgroup.startswith("admin_"):returnself.is_superuserreturnself.is_eighth_admin
@propertydefis_teacher(self)->bool:"""Checks if user is a teacher. Returns: Whether this user is a teacher. """returnself.user_typein("teacher","counselor")@propertydefis_student(self)->bool:"""Checks if user is a student. Returns: Whether this user is a student. """returnself.user_type=="student"@propertydefis_alum(self)->bool:"""Checks if user is an alumnus. Returns: Whether this user is an alumnus. """returnself.user_type=="alum"@propertydefis_senior(self)->bool:"""Checks if user is a student in Grade 12. Returns: Whether this user is a senior. """returnself.is_studentandself.grade_number==12@propertydefis_eighthoffice(self)->bool:"""Checks if user is an Eighth Period office user. This is currently hardcoded, but is meant to be used instead of user.id == 9999 or user.username == "eighthoffice". Returns: Whether this user is an Eighth Period office user. """returnself.id==9999@propertydefis_active(self)->bool:"""Checks if the user is active. This is currently used to catch invalid logins. Returns: Whether the user is "active" -- i.e. their account is not locked. """returnnotself.username.startswith("INVALID_USER")andnotself.user_locked@propertydefis_restricted(self)->bool:"""Checks if user needs the restricted view of Ion This applies to users that are user_type 'user', user_type 'alum' or user_type 'service' Returns: Whether this user should see a restricted view of Ion. """returnself.user_typein["user","alum","service"]@propertydefis_staff(self)->bool:"""Checks if a user should have access to the Django Admin interface. This has nothing to do with staff at TJ - `is_staff` has to be overridden to make this a valid user model. Returns: Whether the user should have access to the Django Admin interface. """returnself.is_superuserorself.has_admin_permission("staff")@propertydefis_attendance_user(self)->bool:"""Checks if user is an attendance-only user. Returns: Whether this user is an attendance-only user. """returnself.user_type=="user"@propertydefis_simple_user(self)->bool:"""Checks if user is a simple user (e.g. eighth office user) Returns: Whether this user is a simple user (e.g. eighth office user). """returnself.user_type=="simple_user"@propertydefhas_senior(self)->bool:"""Checks if a ``Senior`` model (see ``intranet.apps.seniors.models.Senior`` exists for the current user. Returns: Whether a ``Senior`` model (see ``intranet.apps.seniors.models.Senior`` exists for the current user. """returnhasattr(self,"senior")@propertydefis_attendance_taker(self)->bool:"""Checks if this user can take attendance for an eighth activity. Returns: Whether this user can take attendance for an eighth activity. """returnself.is_eighth_adminorself.is_teacherorself.is_attendance_user@propertydefis_eighth_sponsor(self)->bool:"""Determine whether the given user is associated with an. :class:`intranet.apps.eighth.models.EighthSponsor` and, therefore, should view activity sponsoring information. Returns: Whether this user is an eighth period sponsor. """returnEighthSponsor.objects.filter(user=self).exists()@propertydefis_club_officer(self)->bool:"""Checks if this user is an officer of an eighth period activity. Returns: Whether this user is an officer of an eighth period activity. """returnself.officer_for_set.exists()@propertydefis_club_sponsor(self)->bool:"""Used only for club announcements permissions. Not used for eighth period scheduling. Use User.is_eighth_sponsor for that instead."""returnself.club_sponsor_for_set.exists()@propertydeffrequent_signups(self):"""Return a QuerySet of activity id's and counts for the activities that a given user has signed up for more than `settings.SIMILAR_THRESHOLD` times"""key=f"{self.username}:frequent_signups"cached=cache.get(key)ifcached:returncachedfreq_signups=(self.eighthsignup_set.exclude(scheduled_activity__activity__administrative=True).exclude(scheduled_activity__activity__special=True).exclude(scheduled_activity__activity__restricted=True).exclude(scheduled_activity__activity__deleted=True).values("scheduled_activity__activity").annotate(count=Count("scheduled_activity__activity")).filter(count__gte=settings.SIMILAR_THRESHOLD).order_by("-count"))cache.set(key,freq_signups,timeout=60*60*24*7)returnfreq_signups@propertydefrecommended_activities(self):key=f"{self.username}:recommended_activities"cached=cache.get(key)ifcachedisnotNone:returncachedacts=set()forsignupin(self.eighthsignup_set.exclude(scheduled_activity__activity__administrative=True).exclude(scheduled_activity__activity__special=True).exclude(scheduled_activity__activity__restricted=True).exclude(scheduled_activity__activity__deleted=True).exclude(scheduled_activity__block__date__lte=(timezone.localtime()+relativedelta(months=-6)))):acts.add(signup.scheduled_activity.activity)close_acts=set()foractinacts:sim=act.similarities.order_by("-weighted").first()ifsimandsim.weighted>1:close_acts.add(sim.activity_set.exclude(id=act.id).first())cache.set(key,close_acts,timeout=60*60*24*7)returnclose_actsdefarchive_admin_comments(self):current_year=timezone.localdate().yearprevious_year=current_year-1self.admin_comments=f"\n=== {previous_year}-{current_year} comments ===\n{self.admin_comments}"self.save(update_fields=["admin_comments"])
[docs]defget_eighth_sponsor(self):"""Return the ``EighthSponsor`` that this user is associated with. Returns: The ``EighthSponsor`` that this user is associated with. """try:sp=EighthSponsor.objects.get(user=self)exceptEighthSponsor.DoesNotExist:returnFalsereturnsp
[docs]defhas_unvoted_polls(self)->bool:"""Returns whether there are open polls thet this user has not yet voted in. Returns: Whether there are open polls thet this user has not yet voted in. """now=timezone.localtime()returnPoll.objects.visible_to_user(self).filter(start_time__lt=now,end_time__gt=now).exclude(question__answer__user=self).exists()
[docs]defshould_see_polls(self)->bool:""" Returns whether the user should have the Polls icon visible """now=timezone.localtime()returnPoll.objects.visible_to_user(self).filter(start_time__lt=now,end_time__gt=now).exists()orself.has_admin_permission("polls")
[docs]defsigned_up_today(self)->bool:"""If the user is a student, returns whether they are signed up for an activity during all eighth period blocks that are scheduled today. Otherwise, returns ``True``. Returns: If the user is a student, returns whether they are signed up for an activity during all eighth period blocks that are scheduled today. Otherwise, returns ``True``. """ifnotself.is_student:returnTruereturnnotEighthBlock.objects.get_blocks_today().exclude(eighthscheduledactivity__eighthsignup_set__user=self).exists()
[docs]defsigned_up_next_few_days(self,*,num_days:int=3)->bool:"""If the user is a student, returns whether they are signed up for an activity during all eighth period blocks in the next ``num_days`` days. Otherwise, returns ``True``. Today is counted as a day, so ``signed_up_few_next_day(num_days=1)`` is equivalent to ``signed_up_today()``. Args: num_days: The number of days (including today) on which to search for blocks during which the user is signed up. Returns: If the user is a student, returns whether they are signed up for an activity during all eighth period blocks in the next ``num_days`` days. Otherwise, returns ``True``. """ifnotself.is_student:returnTruetoday=timezone.localdate()end_date=today+timedelta(days=num_days-1)return(notEighthBlock.objects.filter(date__gte=today,date__lte=end_date).exclude(eighthscheduledactivity__eighthsignup_set__user=self).exists())
[docs]defabsence_count(self)->int:"""Return the user's absence count. If the user has no absences or is not a signup user, returns 0. Returns: The number of absences this user has. """returnEighthSignup.objects.filter(user=self,was_absent=True,scheduled_activity__attendance_taken=True).count()
[docs]defabsence_info(self):"""Returns a ``QuerySet`` of the ``EighthSignup``s for which this user was absent. Returns: A ``QuerySet`` of the ``EighthSignup``s for which this user was absent. """returnEighthSignup.objects.filter(user=self,was_absent=True,scheduled_activity__attendance_taken=True)
[docs]defhandle_delete(self):"""Handle a graduated user being deleted."""fromintranet.apps.eighth.modelsimportEighthScheduledActivity# pylint: disable=import-outside-toplevelEighthScheduledActivity.objects.filter(eighthsignup_set__user=self).update(archived_member_count=F("archived_member_count")+1)
def__getattr__(self,name):ifname=="properties":returnUserProperties.objects.get_or_create(user=self)[0]elifname=="dark_mode_properties":returnUserDarkModeProperties.objects.get_or_create(user=self)[0]raiseAttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")def__str__(self):returnself.usernameorself.ion_usernameorstr(self.id)def__int__(self):returnself.id
[docs]classUserProperties(models.Model):user=models.OneToOneField(settings.AUTH_USER_MODEL,related_name="properties",on_delete=models.CASCADE)_address=models.OneToOneField("Address",null=True,blank=True,on_delete=models.SET_NULL)_schedule=models.ManyToManyField("Section",related_name="_students")""" User preference permissions (privacy options) When setting permissions, use set_permission(permission, value , parent=False) The permission attribute should be the part after "self_" or "parent_" e.g. show_pictures If you're setting permission of the student, but the parent permission is false, the method will fail and return False. To define a new permission, just create two new BooleanFields in the same pattern as below. """self_show_pictures=models.BooleanField(default=False)parent_show_pictures=models.BooleanField(default=False)self_show_address=models.BooleanField(default=False)parent_show_address=models.BooleanField(default=False)self_show_telephone=models.BooleanField(default=False)parent_show_telephone=models.BooleanField(default=False)self_show_eighth=models.BooleanField(default=False)parent_show_eighth=models.BooleanField(default=False)self_show_schedule=models.BooleanField(default=False)parent_show_schedule=models.BooleanField(default=False)def__getattr__(self,name):ifname.startswith("self")orname.startswith("parent"):returnobject.__getattribute__(self,name)ifname=="address":returnself._addressifself.attribute_is_visible("show_address")elseNoneifname=="schedule":returnself._scheduleifself.attribute_is_visible("show_schedule")elseNoneraiseAttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")def__setattr__(self,name,value):ifname=="address":ifself.attribute_is_visible("show_address"):self._address=valuesuper().__setattr__(name,value)# pylint: disable=no-member; Pylint is wrongdef__str__(self):returnself.user.__str__()
[docs]defset_permission(self,permission:str,value:bool,parent:bool=False,admin:bool=False)->bool:"""Sets permission for personal information. Returns False silently if unable to set permission. Returns True if successful. Args: permission: The name of the permission to set. value: The value to set the permission to. parent: Whether to set the parent's permission instead of the student's permission. If ``parent`` is ``True`` and ``value`` is ``False``, both the parent and the student's permissions will be set to ``False``. admin: If set to ``True``, this will allow changing the student's permission even if the parent's permission is set to ``False`` (normally, this causes an error). """try:ifnotgetattr(self,f"parent_{permission}")andnotparentandnotadmin:returnFalselevel="parent"ifparentelse"self"setattr(self,f"{level}_{permission}",value)update_fields=[f"{level}_{permission}"]# Set student permission to false if parent sets permission to false.ifparentandnotvalue:setattr(self,f"self_{permission}",False)update_fields.append(f"self_{permission}")self.save(update_fields=update_fields)returnTrueexceptExceptionase:logger.error("Error occurred setting permission %s to %s: %s",permission,value,e)returnFalse
[docs]def_current_user_override(self)->bool:"""Return whether the currently logged in user is a teacher, and can view all of a student's information regardless of their privacy settings. Returns: Whether the currently logged in user can view all of a student's information regardless of their privacy settings. """try:# threadlocals is a module, not an actual thread locals objectrequest=threadlocals.request()ifrequestisNone:returnFalserequesting_user=request.userifisinstance(requesting_user,AnonymousUser)ornotrequesting_user.is_authenticated:returnFalsecan_view_anyway=requesting_userand(requesting_user.is_teacherorrequesting_user.is_eighthofficeorrequesting_user.is_eighth_admin)except(AttributeError,KeyError)ase:logger.error("Could not check teacher/eighth override: %s",e)can_view_anyway=Falsereturncan_view_anyway
[docs]defis_http_request_sender(self)->bool:"""Checks if a user the HTTP request sender (accessing own info) Used primarily to load private personal information from the cache. (A student should see all info on his or her own profile regardless of how the permissions are set.) Returns: Whether the user is the sender of the current HTTP request. """try:# threadlocals is a module, not an actual thread locals objectrequest=threadlocals.request()ifrequestandrequest.userandrequest.user.is_authenticated:requesting_user_id=request.user.idreturnstr(requesting_user_id)==str(self.user.id)except(AttributeError,KeyError)ase:logger.error("Could not check request sender: %s",e)returnFalsereturnFalse
[docs]defattribute_is_visible(self,permission:str)->bool:"""Checks privacy options to see if an attribute is visible to the user sending the current HTTP request. Args: permission: The name of the permission to check. Returns: Whether the user sending the current HTTP request has permission to view the given permission. """try:parent=getattr(self,f"parent_{permission}")student=getattr(self,f"self_{permission}")exceptException:logger.error("Could not retrieve permissions for %s",permission)return(parentandstudent)or(self.is_http_request_sender()orself._current_user_override())
[docs]defattribute_is_public(self,permission:str)->bool:"""Checks if attribute is visible to public (ignoring whether the user sending the HTTP request has permission to access it). Args: permission: The name of the permission to check. Returns: Whether the given permission is public. """try:parent=getattr(self,f"parent_{permission}")student=getattr(self,f"self_{permission}")exceptException:logger.error("Could not retrieve permissions for %s",permission)returnparentandstudent
[docs]classUserDarkModeProperties(models.Model):""" Contains user properties relating to dark mode """user=models.OneToOneField(settings.AUTH_USER_MODEL,related_name="dark_mode_properties",on_delete=models.CASCADE)dark_mode_enabled=models.BooleanField(default=False)def__str__(self):returnstr(self.user)
[docs]classEmail(models.Model):"""Represents an email address"""address=models.EmailField()user=models.ForeignKey(settings.AUTH_USER_MODEL,related_name="emails",on_delete=models.CASCADE)def__str__(self):returnself.addressclassMeta:unique_together=("user","address")
[docs]classPhone(models.Model):"""Represents a phone number NOTE: This model is no longer used because of privacy reasons. """PURPOSES=(("h","Home Phone"),("m","Mobile Phone"),("o","Other Phone"))purpose=models.CharField(max_length=1,choices=PURPOSES,default="o")user=models.ForeignKey(settings.AUTH_USER_MODEL,related_name="phones",on_delete=models.CASCADE)_number=PhoneField()# validators should be a listdef__setattr__(self,name,value):ifname=="number":ifself.user.properties.attribute_is_visible("show_telephone"):self._number=valueself.save(update_fields=["_number"])else:super().__setattr__(name,value)# pylint: disable=no-member; Pylint is wrongdef__getattr__(self,name):ifname=="number":returnself._numberifself.user.properties.attribute_is_visible("show_telephone")elseNoneraiseAttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")def__str__(self):returnf"{self.get_purpose_display()}: {self.number}"classMeta:unique_together=("user","_number")
[docs]classWebsite(models.Model):"""Represents a user's website NOTE: This model is no longer used because of privacy reasons. """url=models.URLField()user=models.ForeignKey(settings.AUTH_USER_MODEL,related_name="websites",on_delete=models.CASCADE)def__str__(self):returnself.urlclassMeta:unique_together=("user","url")
[docs]classAddress(models.Model):"""Represents a user's address. Attributes: street The street name of the address. city The city name of the address. state The state name of the address. postal_code The zip code of the address. NOTE: This model is no longer used because of privacy reasons. """street=models.CharField(max_length=255)city=models.CharField(max_length=40)state=models.CharField(max_length=20)postal_code=models.CharField(max_length=20)
[docs]def__str__(self):"""Returns full address string."""returnf"{self.street}\n{self.city}, {self.state}{self.postal_code}"
[docs]classPhoto(models.Model):"""Represents a user photo"""grade_number=models.IntegerField(choices=GRADE_NUMBERS)_binary=models.BinaryField()user=models.ForeignKey(settings.AUTH_USER_MODEL,related_name="photos",on_delete=models.CASCADE)def__setattr__(self,name,value):ifname=="binary":ifself.user.properties.attribute_is_visible("show_pictures"):self._binary=valueself.save(update_fields=["_binary"])else:super().__setattr__(name,value)# pylint: disable=no-member; Pylint is wrongdef__getattr__(self,name):ifname=="binary":returnself._binaryifself.user.properties.attribute_is_visible("show_pictures")elseNoneraiseAttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")@cached_propertydefbase64(self)->Optional[bytes]:"""Returns base64 encoded binary data for a user's picture. Returns: Base 64-encoded binary data for a user's picture. """binary=self.binaryifbinary:returnb64encode(binary)returnNone
[docs]classGrade:"""Represents a user's grade."""names=[elem[1]foreleminGRADE_NUMBERS]
[docs]def__init__(self,graduation_year):"""Initialize the Grade object. Args: graduation_year The numerical graduation year of the user """ifgraduation_yearisNone:self._number=13else:self._number=get_senior_graduation_year()-int(graduation_year)+12if9<=self._number<=12:self._name=next(elem[1]foreleminGRADE_NUMBERSifelem[0]==self._number)else:self._name="graduate"
@propertydefnumber(self)->int:"""Return the grade as a number (9-12). For use in templates since there is no nice integer casting. In Python code you can also use int() on a Grade object. """returnself._number@propertydefname(self)->str:"""Return the grade's name (e.g. senior)"""returnself._name@propertydefname_plural(self)->str:"""Return the grade's plural name (e.g. freshmen)"""return"freshmen"if(self._numberandself._number==9)elsef"{self._name}s"ifself._nameelse""@propertydeftext(self)->str:"""Return the grade's number as a string (e.g. Grade 12, Graduate)"""if9<=self._number<=12:returnf"Grade {self._number}"else:returnself._name@staticmethoddefnumber_from_name(name:str)->Optional[int]:ifnameinGrade.names:returnGrade.names.index(name)+9returnNone@classmethoddefgrade_from_year(cls,graduation_year:int)->int:today=timezone.localdate()iftoday.month>=settings.YEAR_TURNOVER_MONTH:current_senior_year=today.year+1else:current_senior_year=today.yearreturncurrent_senior_year-graduation_year+12@classmethoddefyear_from_grade(cls,grade:int)->int:today=timezone.localdate()iftoday.month>settings.YEAR_TURNOVER_MONTH:current_senior_year=today.year+1else:current_senior_year=today.yearreturncurrent_senior_year+12-grade
[docs]def__int__(self):"""Return the grade as a number (9-12)."""returnself._number
[docs]def__str__(self):"""Return name of the grade."""returnself._name
[docs]classCourse(models.Model):"""Represents a course at TJ (not to be confused with section) NOTE: This model is no longer used because of privacy reasons. """name=models.CharField(max_length=50)course_id=models.CharField(max_length=12,unique=True)def__str__(self):returnf"{self.name} ({self.course_id})"classMeta:ordering=("name","course_id")
[docs]classSection(models.Model):"""Represents a section - a class with teacher, period, and room assignments NOTE: This model is no longer used because of privacy reasons. """course=models.ForeignKey(Course,related_name="sections",on_delete=models.CASCADE)teacher=models.ForeignKey(settings.AUTH_USER_MODEL,null=True,blank=True,on_delete=models.SET_NULL)room=models.CharField(max_length=16)period=models.IntegerField()section_id=models.CharField(max_length=16,unique=True)sem=models.CharField(max_length=2)def__str__(self):return"{} ({}) - {} Pd. {}".format(self.course.name,self.section_id,self.teacher.full_nameifself.teacherelse"Unknown",self.period)def__getattr__(self,name):ifname=="students":return[s.userforsinself._students.all()ifs.attribute_is_visible("show_schedule")]raiseAttributeError(f"{type(self).__name__!r} object has no attribute {name!r}")classMeta:ordering=("section_id","period")