class Transaction(models.Model): """ Model that stores the data for every transaction, to allow better monitoring of the status of the whole system. Each change to any piece of gear should be done via a transaction manager function. This allows transactions to serve as state change vectors for the system, and the status of the system at any time could be reconstructed from the addition (subsequent application) of these transactions. NEVER CREATE TRANSACTIONS MANUALLY, ALWAYS USE THE MANAGER FUNCTIONS """ objects = TransactionManager() transaction_types = [ ("Rental", ( ("CheckOut", "Check Out"), ("CheckIn", "Check In"), ("Inventory", "In Stock") ) ), ("Admin Actions", ( ("Create", "New Gear"), ("Delete", "Remove Gear"), ("ReTag", "Change Tag"), ("Break", "Set Broken"), ("Fix", "Set Fixed"), ("Override", "Admin Change") ) ), ("Auto Updates", ( ("Missing", "Gear Missing"), ("Expire", "Gear Expiration"), ) ) ] primary_key = PrimaryKeyField() #: The time at which this transaction was created - will be automatically set and cannot be changed timestamp = models.DateTimeField(auto_now_add=True) #: A string defining the type of transaction this is (see transaction types) type = models.CharField(max_length=20, choices=transaction_types) #: The piece of gear this transaction relates to: MUST EXIST gear = models.ForeignKey(Gear, null=False, on_delete=models.PROTECT, related_name="has_checked_out", validators=[validate_available]) #: If this transaction relates to a member, that member should be referenced here member = models.ForeignKey(Member, null=True, on_delete=models.PROTECT) #: Either SYSTEM or a String of the rfid of the person who authorized the transaction authorizer = models.ForeignKey(Member, null=False, on_delete=models.PROTECT, related_name="has_authorized", validators=[validate_auth]) #: Any additional notes to be saved about this transaction comments = models.TextField(default="") def __str__(self): return "{} Transaction for a {}".format(self.type, self.gear.name) @property def detail_url(self): return reverse("admin:core_transaction_detail", kwargs={"pk": self.pk})
class Member(AbstractBaseUser, PermissionsMixin): """This is the base model for all members (this includes staffers)""" objects = MemberManager() primary_key = PrimaryKeyField() first_name = models.CharField(max_length=50, null=True) last_name = models.CharField(max_length=50, null=True) email = models.EmailField(verbose_name="email address", max_length=255, unique=True) rfid = RFIDField(verbose_name="RFID") image = models.ImageField( verbose_name="Profile Picture", default="shaka.webp", upload_to=get_profile_pic_upload_location, blank=True, ) phone_number = PhoneNumberField(unique=False, null=True) date_joined = models.DateField(auto_now_add=True) date_expires = models.DateField(null=False) is_admin = models.BooleanField(default=False) group = models.CharField(default="Unset", max_length=30) #: This is used by django to determine if users are allowed to login. Leave it, except when banishing someone is_active = models.BooleanField( default=True ) # Use is_active_member to check actual activity certifications = models.ManyToManyField(Certification, blank=True) USERNAME_FIELD = "email" REQUIRED_FIELDS = ["date_expires"] @property def is_active_member(self): """Return true if the member has a valid membership""" return self.has_permission("core.is_active_member") @property def is_staff(self): """ Property that is used by django to determine whether a user is allowed to log in to the admin: i.e. everyone """ return True @property def edit_profile_url(self): return reverse("admin:core_member_change", kwargs={"object_id": self.pk}) @property def view_profile_url(self): return reverse("admin:core_member_detail", kwargs={"pk": self.pk}) def has_name(self): """Check whether the name of this member has been set""" return self.first_name and self.last_name def get_full_name(self): """Return the full name if it is know, or 'New Member' if it is not""" if self.has_name(): return f"{self.first_name} {self.last_name}" else: return "New Member" get_full_name.short_description = "Full Name" def get_short_name(self): # The user is identified by their email address return self.first_name def get_all_certifications(self): all_certs = self.certifications.all() return all_certs def has_no_certifications(self): return len(self.certifications.all()) == 0 def __str__(self): """ If we know the name of the user, then display their name, otherwise use their email """ if self.has_name(): return self.get_full_name() else: return self.email def update_admin(self): """Updates the admin status of the user in the django system""" self.is_admin = self.groups.name == "Admin" def expire(self): """Expires this member's membership""" self.move_to_group("Expired") def promote_to_active(self): """Move the member to the group of active members""" self.move_to_group("Member") def extend_membership(self, duration, rfid="", password=""): """Add the given amount of time to this member's membership, and optionally update their rfid and password""" self.move_to_group("Just Joined") if self.date_expires < datetime.date(now()): self.date_expires = now() + duration else: self.date_expires += duration if rfid: self.rfid = rfid if password: self.set_password(password) return self def send_email(self, title, body, from_email, email_host_password): """Sends an email to the member""" send_mail( title, body, from_email, [self.email], fail_silently=False, auth_user=from_email, auth_password=email_host_password, ) def send_membership_email(self, title, body): """Send an email to the member from the membership email""" self.send_email( title, body, settings.MEMBERSHIP_EMAIL_HOST_USER, settings.MEMBERSHIP_EMAIL_HOST_PASSWORD, ) def send_intro_email(self, finish_signup_url): """Send the introduction email with the link to finish signing up to the member""" title = "Finish Signing Up" # get the absolute path equivalent of going up one level and then into the templates directory templates_dir = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir, "templates") ) template_file = open(os.path.join(templates_dir, "emails", "intro_email.txt")) template = template_file.read() body = template.format(finish_signup_url=finish_signup_url) self.send_membership_email(title, body) def send_expire_soon_email(self): """Send an email warning the member that their membership will soon expire""" title = "Excursion Club Membership Expiring Soon!" templates_dir = os.path.abspath( os.path.join(os.path.dirname(__file__), os.pardir, "templates") ) template_file = open(os.path.join(templates_dir, "emails", "intro_email.txt")) template = template_file.read() body = template.format( member_name=self.get_full_name(), expiration_date=self.date_expires ) self.send_membership_email(title, body) def has_module_perms(self, app_label): """This is required by django, determine whether the user is allowed to view the app""" return True def has_permission(self, permission_name): """Loop through all the permissions of the group associated with this member to see if they have this one""" return self.has_perm(permission_name) def move_to_group(self, group_name): """ Convenience function to move a member to a group Always use this function since it changes the group and the group shortcut field """ new_group = Group.objects.filter(name=group_name) self.groups.set(new_group) self.group = str(new_group[0]) self.save()
class Gear(models.Model): """ The base model for a piece of gear """ objects = GearManager() class Meta: verbose_name_plural = "Gear" primary_key = PrimaryKeyField() rfid = models.CharField(max_length=10, unique=True) image = models.ForeignKey(AlreadyUploadedImage, on_delete=models.CASCADE) status_choices = [ (0, "In Stock" ), # Ready and available in the gear sheds, waiting to be used (1, "Checked Out" ), # Somebody has it right now, but it should soon be available again ( 2, "Broken" ), # It is broken to the point it should not be checked out, waiting for repair ( 3, "Missing" ), # Has been checked out for a while, time to yell at a member to give it back (4, "Dormant" ), # Missing for very long time: assume it has been lost until found (5, "Removed" ), # It is gone, dead and never coming back. Always set manually ] #: The status determines what transactions the gear can participate in and where it is visible status = models.IntegerField(choices=status_choices) #: Who currently has this piece of gear. If null, then the gear is not checked out checked_out_to = models.ForeignKey(Member, blank=True, null=True, on_delete=models.SET_NULL) #: The date at which this gear is due to be returned, null if not checked out due_date = models.DateField(blank=True, null=True, default=None) geartype = models.ForeignKey(GearType, on_delete=models.CASCADE) gear_data = models.CharField(max_length=2000) def __str__(self): return self.name def __getattr__(self, item): """ Allows the values of CustomDataFields stored in GearType to be accessed as if they were attributes of Gear """ gear_data = json.loads(self.__getattribute__('gear_data')) if item is None: return self elif item in gear_data.keys(): geartype = self.__getattribute__('geartype') field = geartype.data_fields.get(name=item) return field.get_value(gear_data[item]) else: raise AttributeError(f'No attribute {item} for {repr(self)}!') def get_display_gear_data(self): """Return the gear data as a simple dict of field_name, field_value""" simple_data = {} attr_fields = self.geartype.data_fields.all() gear_data = json.loads(self.gear_data) for field in attr_fields: simple_data[field.name] = field.get_str(gear_data[field.name]) return simple_data @property def edit_gear_url(self): return reverse("admin:core_gear_change", kwargs={"object_id": self.pk}) def get_extra_fieldset(self, name="Additional Data", classes=('wide', )): """Get a fieldset that contains data on how to represent the extra data fields contained in geartype""" fieldset = (name, { 'classes': classes, 'fields': self.geartype.get_field_names() }) return fieldset def get_status(self): return self.status_choices[self.status][1] @property def image_url(self): if self.image and hasattr(self.image, 'url'): return self.image.url @property def name(self): """ Auto-generate a name that can (semi-uniquely) identify this piece of gear Name will be in the form: <GearType> - <attr 1>, <attr 2>, etc... """ # Get all custom data fields for this data_type, except those that contain a rfid attr_fields = self.geartype.data_fields.exclude(data_type='rfid') attributes = [] gear_data = json.loads(self.gear_data) for field in attr_fields: string = field.get_str(gear_data[field.name]) if string: attributes.append(str(string)) if attributes: attr_string = ", ".join(attributes) name = f'{self.geartype.name} - {attr_string}' else: name = self.geartype.name return name def get_department(self): return self.geartype.department get_department.short_description = "Department" def is_available(self): """Returns True if the gear is available for renting""" return True if self.status == 0 else False def is_rented_out(self): return True if self.status == 1 else False def is_active(self): """Returns True if the gear is actively in circulation (ie could be checked out in a few days)""" if self.status <= 1: return True else: return False def is_existent(self): """Returns True if the gear has not been removed or lost""" if self.status <= 3: return True else: return False
class Member(AbstractBaseUser, PermissionsMixin): """This is the base model for all members (this includes staffers)""" objects = MemberManager() primary_key = PrimaryKeyField() # Personal contact information first_name = models.CharField(max_length=50, null=True) last_name = models.CharField(max_length=50, null=True) email = models.EmailField(verbose_name="email address", max_length=255, unique=True) image = models.ImageField( verbose_name="Profile Picture", default=settings.DEFAULT_IMG, upload_to=get_profile_pic_upload_location, blank=True, null=True, ) phone_number = PhoneNumberField(unique=False, null=True) # Emergency contact information emergency_contact_name = models.CharField(max_length=100, verbose_name="Contact Name", null=True) emergency_relation = models.CharField(max_length=50, verbose_name="Relationship", null=True) emergency_phone = PhoneNumberField(unique=False, verbose_name="Phone Number", null=True) emergency_email = models.EmailField(unique=False, verbose_name="Best Email", null=True) # Membership data date_joined = models.DateField(auto_now_add=True) date_expires = models.DateField(null=False) rfid = RFIDField(verbose_name="RFID") group = models.CharField(default="Unset", max_length=30) is_admin = models.BooleanField(default=False) certifications = models.ManyToManyField(Certification, blank=True) #: This is used by django to determine if users are allowed to login. Leave it, except when banishing someone is_active = models.BooleanField( default=True) # Use is_active_member to check actual activity USERNAME_FIELD = "email" REQUIRED_FIELDS = ["date_expires"] @property def is_active_member(self): """Return true if the member has a valid membership""" return self.has_permission("core.is_active_member") @property def is_staff(self): """ Property that is used by django to determine whether a user is allowed to log in to the admin: i.e. everyone """ return True @property def is_staffer(self): """Property to check if a member is a excursion staffer or not""" return self.group in ['Staff', 'Board', 'Admin'] @property def edit_profile_url(self): return reverse("admin:core_member_change", kwargs={"object_id": self.pk}) @property def view_profile_url(self): return reverse("admin:core_member_detail", kwargs={"pk": self.pk}) @property def make_staff_url(self): return reverse('admin:core_staffer_add', kwargs={'member': self}) def has_name(self): """Check whether the name of this member has been set""" return self.first_name and self.last_name def get_full_name(self): """Return the full name if it is know, or 'New Member' if it is not""" if self.has_name(): return f"{self.first_name} {self.last_name}" else: return "New Member" get_full_name.short_description = "Full Name" def get_short_name(self): # The user is identified by their email address return self.first_name def get_all_certifications(self): all_certs = self.certifications.all() return all_certs def has_no_certifications(self): return len(self.certifications.all()) == 0 def __str__(self): """ If we know the name of the user, then display their name, otherwise use their email """ if self.has_name(): return self.get_full_name() else: return self.email def update_admin(self): """Updates the admin status of the user in the django system""" self.is_admin = self.groups.name == "Admin" def expire(self): """Expires this member's membership""" self.move_to_group("Expired") def promote_to_active(self): """Move the member to the group of active members""" if self.group == "Staff" or self.group == "Board" or self.group == "Admin": print("Member status is already better than member") else: self.move_to_group("Member") def extend_membership(self, duration, rfid="", password=""): """Add the given amount of time to this member's membership, and optionally update their rfid and password""" self.move_to_group("Just Joined") if self.date_expires < datetime.date(now()): self.date_expires = now() + duration else: self.date_expires += duration if rfid: self.rfid = rfid if password: self.set_password(password) return self def send_email(self, title, body, from_email): """Sends an email to the member""" emailing.send_email([self.email], title, body, from_email=from_email, from_name='Excursion Club', receiver_names=[self.get_full_name()]) def send_membership_email(self, title, body): """Send an email to the member from the membership email""" emailing.send_membership_email([self.email], title, body, receiver_names=[self.get_full_name()]) def send_intro_email(self, finish_signup_url): """Send the introduction email with the link to finish signing up to the member""" title = "Finish Signing Up" template = get_email_template('intro_email') body = template.format(finish_signup_url=finish_signup_url) self.send_membership_email(title, body) def send_expires_soon_email(self): """Send an email warning the member that their membership will soon expire""" title = "Climbing Club Membership Expiring Soon!" template = get_email_template('expire_soon_email') body = template.format(member_name=self.get_full_name(), expiration_date=self.date_expires) self.send_membership_email(title, body) def send_expired_email(self): """Send an email warning the member that their membership will soon expire""" title = "Climbing Club Membership Expired!" template = get_email_template('expired_email') body = template.format(member_name=self.get_full_name(), today=self.date_expires) self.send_membership_email(title, body) def send_missing_gear_email(self, all_gear): """Send an email to member that they have gear to return""" gear_rows = [] for gear in all_gear: gear_rows.append( f"<tr><td>{gear.name}</td><td>{gear.due_date.strftime('%a, %b %d, %Y')}</td></tr>" ) template = get_email_template('missing_gear') body = template.format(first_name=self.first_name, gear_rows="".join(gear_rows)) title = 'Gear Overdue' self.send_email( title, body, '*****@*****.**', ) def send_new_staff_email(self, staffer): """Sen an email welcoming the member to staff""" title = "Welcome to staff!" template = get_email_template('new_staffer') body = template.format(member_name=self.first_name, finish_url=settings.WEB_BASE + staffer.edit_profile_url, staffer_email=staffer.exc_email) self.send_membership_email(title, body) def has_module_perms(self, app_label): """This is required by django, determine whether the user is allowed to view the app""" return True def has_permission(self, permission_name): """Loop through all the permissions of the group associated with this member to see if they have this one""" return self.has_perm(permission_name) def move_to_group(self, group_name): """ Convenience function to move a member to a group Always use this function since it changes the group and the group shortcut field """ new_group = Group.objects.filter(name=group_name) self.groups.set(new_group) self.group = str(new_group[0]) self.save()