Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add attendance display #577

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions collectives/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ def models_to_js():
for name, obj in inspect.getmembers(sys.modules["collectives.models"]):
if inspect.isclass(obj) and issubclass(obj, ChoiceEnum):
enums = enums + "const Enum" + name + "=" + obj.js_values() + ";"
enums = enums + "const Enum" + name + "Keys=" + obj.js_keys() + ";"

enums = enums + "const EnumActivityType=" + ActivityType.js_values() + ";"
enums = enums + "const EnumActivityTypeKeys=" + ActivityType.js_keys() + ";"
enums = enums + "const EnumEventType=" + EventType.js_values() + ";"
enums = enums + "const EnumEventTypeKeys=" + EventType.js_keys() + ";"

tags = ",".join([f"{i}:'{tag['name']}'" for i, tag in EventTag.all().items()])
enums = enums + "const EnumEventTag={" + tags + "};"
Expand Down
11 changes: 11 additions & 0 deletions collectives/models/activity_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,17 @@ def js_values(cls):
items = [f"{type.id}:'{escape(type.name)}'" for type in types]
return "{" + ",".join(items) + "}"

@classmethod
def js_keys(cls):
"""Class method to cast Activity keys as js dict

:return: all activity types as js Dictionnary
:rtype: String
"""
types = cls.get_all_types()
items = [f'"{type.short}" : {type.id}' for type in types]
return "{" + ",".join(items) + "}"


def activities_without_leader(activities, leaders):
"""Check if leaders has right to lead it.
Expand Down
16 changes: 16 additions & 0 deletions collectives/models/event/event_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ class EventType(db.Model):
:type: string
"""

attendance = db.Column(db.Boolean(), nullable=False, default=True)
""" True if this event acts on the users attendance reports.

:type: Boolean"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"track_attendance" peut être ? Histoire d'avoir un nom un peu plus explicite

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

En effet, je vais modifier


def has_valid_license(self, user):
"""Check whether an user has a valic license for this type of event

Expand Down Expand Up @@ -95,3 +100,14 @@ def js_values(cls):
types = cls.get_all_types()
items = [f"{type.id}:'{escape(type.name)}'" for type in types]
return "{" + ",".join(items) + "}"

@classmethod
def js_keys(cls):
"""Class method to cast all event type keys as js dict

:return: all activity types as js Dictionnary
:rtype: String
"""
types = cls.get_all_types()
items = [f'"{type.short}" : {type.id}' for type in types]
return "{" + ",".join(items) + "}"
57 changes: 56 additions & 1 deletion collectives/models/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,61 @@ def valid_transitions(self, requires_payment):
"""
return self.__class__.transition_table(requires_payment)[self.value]

@classmethod
def colors(cls):
"""
:return: a dict defining display colors for all enum values
:rtype: dict
"""
return {
cls.Active: "#00a5cc",
cls.Rejected: "#005aa0",
cls.PaymentPending: "#eee8a9",
cls.SelfUnregistered: "#f49431",
cls.JustifiedAbsentee: "#005aa0",
cls.UnJustifiedAbsentee: "#ffad8f",
cls.ToBeDeleted: "",
cls.Waiting: "#e6f4f1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Si possible ce serait pas mal de définir les couleurs au niveau des templates Jinja2

}

def color(self):
"""Select the value color.

:returns: a CSS color
:rtype: string"""
cls = self.__class__
return cls.colors()[self.value]

@classmethod
def absent_status(cls):
"""
:return: a list of values defined as absent status
:rtype: list
"""
return [
cls.PaymentPending,
cls.SelfUnregistered,
cls.JustifiedAbsentee,
cls.UnJustifiedAbsentee,
]

def is_absent(self):
"""Check if this status is considered as absent.

:rtype: bool"""
return self in self.__class__.absent_status()

@classmethod
def infamous_status(cls):
"""
:return: a list of values defined as infamous status
:rtype: list
"""
return [
cls.SelfUnregistered,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pour moi SelfUnregistered ne devrait pas être infamous par défaut, je pense qu'il y a beaucoup d'encadrants qui ne mettent pas à jour le statut pour une absence justifiée. Si elle est vraiment non justifié ils peuvent changer le statut en UnjustifiedAbsentee a posteriori. Mieux vaut avoir des faux négatifs que des faux positifs à mon avis.

cls.UnJustifiedAbsentee,
]


class Registration(db.Model):
"""Object linking a user (participant) and an event.
Expand Down Expand Up @@ -139,7 +194,7 @@ class Registration(db.Model):
:type: :py:class:`collectives.models.registration.RegistrationStatus`"""

level = db.Column(
db.Enum(RegistrationLevels), nullable=False
db.Enum(RegistrationLevels), nullable=False, default=RegistrationLevels.Normal
) # Co-encadrant, Normal
""" Level of the participant for this event (normal, co-leader...)

Expand Down
61 changes: 60 additions & 1 deletion collectives/models/user/misc.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
""" Module for misc User methods which does not fit in another submodule"""
import os
import datetime

import phonenumbers
from flask_uploads import UploadSet, IMAGES
from sqlalchemy import func

from collectives.models.globals import db
from collectives.models.event import Event, EventType
from collectives.models.configuration import Configuration
from collectives.models.registration import Registration
from collectives.models.registration import Registration, RegistrationStatus
from collectives.models.reservation import ReservationStatus
from collectives.models.user.enum import Gender
from collectives.utils.time import current_time
Expand Down Expand Up @@ -211,3 +213,60 @@ def form_of_address(self):
if self.gender == Gender.Man:
return "M."
return ""

def attendance_report(self, time=datetime.timedelta(days=99 * 365)):
"""Compile an attendance report of the user by status.

Only Event types Collectives and Training are looked.

:returns: attendance report
:rtype: dict
:param datetime.timedelta time: How far in the past the registrations should be
searched.
"""
start_date = datetime.datetime.now() - time
query = Registration.query.with_entities(
Registration.status, func.count(Registration.status)
)
query = query.filter_by(user=self)
query = query.filter(Registration.event.has(Event.start > start_date))
query = query.filter(
Registration.event.has(Event.event_type.has(EventType.attendance == True))
)
return dict(query.group_by(Registration.status).all())

def attendance_grade(self, time=datetime.timedelta(days=99 * 365)):
"""Compile an attendance grade of the user by status, from A to E.

Basically, it counts infamous registration status. Algorithms select the most
favorable. For the time period:

* A: No infamous status
* B: Less than 5% infamous status.
* C: One infamous status
* D: Two infamous status
* E: others

:param datetime.timedelta time: How far in the past the registrations should be
searched.
:returns: A for good attendance, F for awful
:rtype: String
"""
report = self.attendance_report(time)
infamous_strikes = sum(
report.get(status, 0) for status in RegistrationStatus.infamous_status()
)

if infamous_strikes == 0:
return "A"

total = sum(report.values())
percent = infamous_strikes / total

if percent < 0.05:
return "B"
if infamous_strikes == 1:
return "C"
if infamous_strikes == 2:
return "D"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quelle est la motivation pour définir parfois en pourcentage et parfois en nombre absolu ? Tu as un exemple en tête ?

return "E"
10 changes: 10 additions & 0 deletions collectives/models/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ def js_values(cls):
items = [str(s.value) + ":'" + str(escape(s.display_name())) + "'" for s in cls]
return "{" + ",".join(items) + "}"

@classmethod
def js_keys(cls):
"""Class method to cast Enum keys as js dict

:return: enum as js Dictionnary
:rtype: String
"""
items = [f'"{s.name}" : {s.value}' for s in cls]
return "{" + ",".join(items) + "}"

@classmethod
def coerce(cls, item):
"""Check if an item if part the Enum
Expand Down
2 changes: 2 additions & 0 deletions collectives/static/css/components/_index.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import 'bar';
@import 'box';
@import 'buttons';
@import 'collective-display';
Expand All @@ -12,6 +13,7 @@
@import 'pagination';
@import 'user-image';
@import 'table';
@import 'tooltip';
@import 'modal';
@import 'modal_form';
@import 'card';
Expand Down
18 changes: 18 additions & 0 deletions collectives/static/css/components/bar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.bar {
box-shadow: $box-shadow-default;
background: $color-white;
padding: 0px;
display:flex;
max-width: 800px;
margin: auto;
text-align: center;

& .item{
padding: 0.5em 0px;
width: 1px;
display: flex;
align-items: center;
justify-content: center;
color: $color-gray-dark;
}
}
1 change: 1 addition & 0 deletions collectives/static/css/components/buttons.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.button, a.button, input[type=button].button, input[type=submit].button{
display: inline-block;
padding: $padding-buttons-default;
margin: $margin-buttons-default;
background: $color-white;
text-transform: uppercase;
text-align: center;
Expand Down
43 changes: 43 additions & 0 deletions collectives/static/css/components/tooltip.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;

& .tooltiptext {
visibility: hidden;
background-color: $color-gray-bg-light;
padding: 5px;
border-radius: 6px;
box-shadow: $box-shadow-default;
min-width: 200px;
color: $color-gray-dark;

/* Position the tooltip text - see examples below! */
position: absolute;
z-index: 1;

&.tooltip-right{
top: -5px;
right: 105%;
}
&.tooltip-left{
top: -5px;
left: 105%;
}
&.tooltip-bottom{
top: 120%;
left: 50%;
}
&.tooltip-top{
bottom: 120%;
left: 50%;
}
}

/* Show the tooltip text when you mouse over the tooltip container */
&:hover .tooltiptext {
visibility: visible;
}
}


38 changes: 19 additions & 19 deletions collectives/static/css/components/usericon.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,26 @@
padding: 4px;
font-size: $font-size-s;
vertical-align: middle;
}

.pending-renewal {
color: $color-white;
background-color: $color-error-dark;
margin-right: 3px;

img {
height: 16px;
vertical-align: middle;
&.pending-renewal, &.attendance {
color: $color-white;
background-color: $color-error-dark;
margin-right: 3px;

img {
height: 16px;
vertical-align: middle;
}
}

&.youth {
color: $color-white;
background: $color-orange-primary-light;
}

&.minor {
color: $color-white;
background: $color-orange-secondary-dark;
}
}

.youth {
color: $color-white;
background: $color-orange-primary-light;
}

.minor {
color: $color-white;
background: $color-orange-secondary-dark;
}
}
1 change: 1 addition & 0 deletions collectives/static/css/variables/padding.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ $padding-s: $padding-vertical-s $padding-horizontal-s;
$padding-m: $padding-vertical-m $padding-horizontal-m;
$padding-buttons-default: $padding-vertical-m $padding-horizontal-m;

$margin-buttons-default: ($padding-vertical-default /2 ) ($padding-vertical-default /2 );

$padding-input-vertical-default: 0.8rem;
$padding-input-horizontal-default: 1rem;
Expand Down
1 change: 1 addition & 0 deletions collectives/static/img/icon/ionicon/md-warning-white.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading