diff --git a/api/admin.py b/api/admin.py index a7788521..ad69918c 100644 --- a/api/admin.py +++ b/api/admin.py @@ -51,17 +51,39 @@ class ShiftInviteAdmin(admin.ModelAdmin): class ClockinAdmin(admin.ModelAdmin): list_display = ('id', 'employee', 'started_at', 'ended_at', 'shift', 'author') - search_fields = ('employee__user__first_name', 'employee__user__last_name', 'employee__user__email', 'author__user__first_name', 'author__user__last_name') + search_fields = ( + 'employee__user__first_name', 'employee__user__last_name', 'employee__user__email', 'author__user__first_name', + 'author__user__last_name') list_filter = ('status',) list_per_page = 100 +class EmployeeDocumentAdmin(admin.ModelAdmin): + list_display = ('id', 'document', 'get_name', 'status', 'created_at', 'updated_at') + search_fields = ( + 'state', 'name', 'employee__user__first_name', 'employee__user__last_name', 'employee__user__email') + list_filter = ('status','document_type__validates_identity','document_type__validates_employment','document_type__is_form') + list_per_page = 100 + + def get_name(self, obj): + return obj.document_type.title if obj.document_type is not None else 'Missing document type' +admin.site.register(EmployeeDocument, EmployeeDocumentAdmin) +class DocumentAdmin(admin.ModelAdmin): + list_display = ('id', 'title', 'validates_identity', 'validates_employment', 'is_form') +admin.site.register(Document, DocumentAdmin) + + + + admin.site.register(Clockin, ClockinAdmin) admin.site.register(UserToken) admin.site.register(Notification) admin.site.register(JobCoreInvite) admin.site.register(BankAccount) -admin.site.register(EmployeeDocument) + +class AppVersionAdmin(admin.ModelAdmin): + list_display = ('id', 'version', 'force_update', 'created_at', 'updated_at') +admin.site.register(AppVersion, AppVersionAdmin) + admin.site.register(PaymentDeduction) -# admin.site.register(City) diff --git a/api/fixtures/development/5-document.yaml b/api/fixtures/development/5-document.yaml new file mode 100644 index 00000000..eb9695ce --- /dev/null +++ b/api/fixtures/development/5-document.yaml @@ -0,0 +1,50 @@ +- model: api.Document + pk: 1 + fields: + title: US Passport + validates_identity: True + validates_employment: True + is_form: False + created_at: "2018-09-13T19:45:00Z" + updated_at: "2018-09-13T19:45:00Z" + +- model: api.Document + pk: 2 + fields: + title: Permanent Resident Card or Alien Registration Receipt Card (Form I-551) + validates_identity: True + validates_employment: True + is_form: False + created_at: "2018-09-13T19:45:00Z" + updated_at: "2018-09-13T19:45:00Z" + +- model: api.Document + pk: 3 + fields: + title: Divers License + validates_identity: True + validates_employment: False + is_form: False + created_at: "2018-09-13T19:45:00Z" + updated_at: "2018-09-13T19:45:00Z" + +- model: api.Document + pk: 4 + fields: + title: I-94 + validates_identity: False + validates_employment: True + is_form: False + created_at: "2018-09-13T19:45:00Z" + updated_at: "2018-09-13T19:45:00Z" + + +- model: api.Document + pk: 5 + fields: + title: W4 Form + validates_identity: False + validates_employment: False + is_form: True + created_at: "2018-09-13T19:45:00Z" + updated_at: "2018-09-13T19:45:00Z" \ No newline at end of file diff --git a/api/management/commands/process_expired.py b/api/management/commands/process_expired.py index 69dc56fd..b36a50f7 100644 --- a/api/management/commands/process_expired.py +++ b/api/management/commands/process_expired.py @@ -9,5 +9,7 @@ def handle(self, *args, **options): #log = [] hooks.process_expired_shifts() + self.stdout.write(self.style.SUCCESS("Successfully expired shifts and clockins")) - self.stdout.write(self.style.SUCCESS("Successfully expired everything")) \ No newline at end of file + hooks.process_expired_documents() + self.stdout.write(self.style.SUCCESS("Successfully expired documents")) \ No newline at end of file diff --git a/api/management/commands/reset_database.py b/api/management/commands/reset_database.py index 1ab764d5..064817e5 100644 --- a/api/management/commands/reset_database.py +++ b/api/management/commands/reset_database.py @@ -57,4 +57,7 @@ def handle(self, *args, **options): log.insert(0, "Deleting PayrollPeriodPayments...") PayrollPeriodPayment.objects.all().delete() + log.insert(0, "Deleting Documents...") + Document.objects.all().delete() + self.stdout.write(self.style.SUCCESS("Successfully deleted all models")) \ No newline at end of file diff --git a/api/migrations/0055_auto_20191217_1631.py b/api/migrations/0055_auto_20191217_1631.py new file mode 100644 index 00000000..97a41331 --- /dev/null +++ b/api/migrations/0055_auto_20191217_1631.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-12-17 16:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_employee_document_active'), + ] + + operations = [ + migrations.AlterField( + model_name='employeedocument', + name='name', + field=models.CharField(blank=True, max_length=150, null=True), + ), + ] diff --git a/api/migrations/0055_auto_20191217_2021.py b/api/migrations/0055_auto_20191217_2021.py new file mode 100644 index 00000000..1ea2eb17 --- /dev/null +++ b/api/migrations/0055_auto_20191217_2021.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.6 on 2019-12-17 20:21 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0054_employee_document_active'), + ] + + operations = [ + migrations.CreateModel( + name='Document', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(blank=True, max_length=50, null=True)), + ('validates_identity', models.BooleanField(default=False)), + ('validates_employment', models.BooleanField(default=False)), + ('is_form', models.BooleanField(default=False)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.RenameField( + model_name='employeedocument', + old_name='state', + new_name='status', + ), + migrations.RemoveField( + model_name='employee', + name='document_active', + ), + migrations.AddField( + model_name='employee', + name='employment_verification_status', + field=models.CharField(blank=True, choices=[('NOT_APPROVED', 'Not Approved'), ('PENDING', 'Pending'), ('BEING_REVIEWED', 'Being Reviews'), ('APPROVED', 'Approved')], default='NOT_APPROVED', max_length=25), + ), + migrations.AddField( + model_name='employeedocument', + name='document_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.Document'), + ), + ] diff --git a/api/migrations/0056_appversion.py b/api/migrations/0056_appversion.py new file mode 100644 index 00000000..c87e08bb --- /dev/null +++ b/api/migrations/0056_appversion.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.4 on 2019-12-17 18:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_auto_20191217_1631'), + ] + + operations = [ + migrations.CreateModel( + name='AppVersion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('version', models.IntegerField(default=94)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + ] diff --git a/api/migrations/0056_auto_20191217_2023.py b/api/migrations/0056_auto_20191217_2023.py new file mode 100644 index 00000000..bdf42dee --- /dev/null +++ b/api/migrations/0056_auto_20191217_2023.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-12-17 20:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0055_auto_20191217_2021'), + ] + + operations = [ + migrations.AlterField( + model_name='document', + name='title', + field=models.CharField(blank=True, max_length=250, null=True), + ), + ] diff --git a/api/migrations/0057_auto_20191217_2044.py b/api/migrations/0057_auto_20191217_2044.py new file mode 100644 index 00000000..ba6e72a5 --- /dev/null +++ b/api/migrations/0057_auto_20191217_2044.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2019-12-17 20:44 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_auto_20191217_2023'), + ] + + operations = [ + migrations.AlterField( + model_name='employeedocument', + name='public_id', + field=models.CharField(max_length=80, null=True), + ), + ] diff --git a/api/migrations/0058_merge_20191217_2131.py b/api/migrations/0058_merge_20191217_2131.py new file mode 100644 index 00000000..6663dee5 --- /dev/null +++ b/api/migrations/0058_merge_20191217_2131.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.6 on 2019-12-17 21:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0056_appversion'), + ('api', '0057_auto_20191217_2044'), + ] + + operations = [ + ] diff --git a/api/migrations/0059_auto_20191218_1649.py b/api/migrations/0059_auto_20191218_1649.py new file mode 100644 index 00000000..b0ebff45 --- /dev/null +++ b/api/migrations/0059_auto_20191218_1649.py @@ -0,0 +1,47 @@ +# Generated by Django 2.2.6 on 2019-12-18 16:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0058_merge_20191217_2131'), + ] + + operations = [ + migrations.RemoveField( + model_name='employeedocument', + name='name', + ), + migrations.AddField( + model_name='appversion', + name='change_log', + field=models.TextField(blank=True, max_length=450), + ), + migrations.AddField( + model_name='appversion', + name='force_update', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='position', + name='description', + field=models.TextField(blank=True, max_length=1050), + ), + migrations.AddField( + model_name='position', + name='meta_description', + field=models.TextField(blank=True, max_length=250), + ), + migrations.AddField( + model_name='position', + name='meta_keywords', + field=models.TextField(blank=True, max_length=250), + ), + migrations.AlterField( + model_name='appversion', + name='version', + field=models.CharField(default=94, max_length=10, unique=True), + ), + ] diff --git a/api/migrations/0060_auto_20191218_1804.py b/api/migrations/0060_auto_20191218_1804.py new file mode 100644 index 00000000..4500bda5 --- /dev/null +++ b/api/migrations/0060_auto_20191218_1804.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.6 on 2019-12-18 18:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0059_auto_20191218_1649'), + ] + + operations = [ + migrations.AddField( + model_name='employeedocument', + name='expired_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AlterField( + model_name='employeedocument', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('ARCHIVED', 'Archived'), ('REJECTED', 'Rejected')], default='PENDING', max_length=8), + ), + ] diff --git a/api/migrations/0060_auto_20191218_1827.py b/api/migrations/0060_auto_20191218_1827.py new file mode 100644 index 00000000..8e3f565c --- /dev/null +++ b/api/migrations/0060_auto_20191218_1827.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.4 on 2019-12-18 18:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0059_auto_20191218_1649'), + ] + + operations = [ + migrations.AddField( + model_name='appversion', + name='build_number', + field=models.IntegerField(default=94), + ), + migrations.AlterField( + model_name='appversion', + name='version', + field=models.CharField(default='94', max_length=10, unique=True), + ), + ] diff --git a/api/migrations/0061_auto_20191218_1824.py b/api/migrations/0061_auto_20191218_1824.py new file mode 100644 index 00000000..8b70d0f4 --- /dev/null +++ b/api/migrations/0061_auto_20191218_1824.py @@ -0,0 +1,38 @@ +# Generated by Django 2.2.6 on 2019-12-18 18:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0060_auto_20191218_1804'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='allowances', + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name='employee', + name='extra_withholding', + field=models.FloatField(blank=True, default=0), + ), + migrations.AddField( + model_name='employee', + name='filing_status', + field=models.CharField(blank=True, choices=[('SINGLE', 'Single'), ('MARRIED_JOINTLY', 'Married filing jointly'), ('MARRIED_SEPARATELY', 'Married filing separately'), ('HEAD', 'Head of household'), ('WIDOWER', 'Qualifying widow(er) with dependent child')], default='SINGLE', max_length=25), + ), + migrations.AlterField( + model_name='employee', + name='employment_verification_status', + field=models.CharField(blank=True, choices=[('NOT_APPROVED', 'Not Approved'), ('PENDING', 'Pending'), ('BEING_REVIEWED', 'Being Reviewed'), ('APPROVED', 'Approved')], default='NOT_APPROVED', max_length=25), + ), + migrations.AlterField( + model_name='employeedocument', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('APPROVED', 'Approved'), ('ARCHIVED', 'Archived'), ('DELETED', 'Deleted'), ('REJECTED', 'Rejected')], default='PENDING', max_length=8), + ), + ] diff --git a/api/migrations/0061_auto_20191218_1830.py b/api/migrations/0061_auto_20191218_1830.py new file mode 100644 index 00000000..7853d2aa --- /dev/null +++ b/api/migrations/0061_auto_20191218_1830.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-12-18 18:30 + +from django.db import migrations + + +def seed_app_version(apps, schema_editor): + AppVersion = apps.get_model('api', 'AppVersion') + AppVersion.objects.create(version='1.1.59') + + +class Migration(migrations.Migration): + dependencies = [ + ('api', '0060_auto_20191218_1827'), + ] + + operations = [ + migrations.RunPython(seed_app_version), + ] diff --git a/api/migrations/0062_merge_20191218_1840.py b/api/migrations/0062_merge_20191218_1840.py new file mode 100644 index 00000000..9e152dcf --- /dev/null +++ b/api/migrations/0062_merge_20191218_1840.py @@ -0,0 +1,9 @@ +# Generated by Django 2.2.4 on 2019-12-18 18:40 +from django.db import migrations +class Migration(migrations.Migration): + dependencies = [ + ('api', '0061_auto_20191218_1824'), + ('api', '0061_auto_20191218_1830'), + ] + operations = [ + ] \ No newline at end of file diff --git a/api/migrations/0063_auto_20191220_0006.py b/api/migrations/0063_auto_20191220_0006.py new file mode 100644 index 00000000..2091e5f8 --- /dev/null +++ b/api/migrations/0063_auto_20191220_0006.py @@ -0,0 +1,44 @@ +# Generated by Django 2.2.4 on 2019-12-20 00:06 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0062_merge_20191218_1840'), + ] + + operations = [ + migrations.AddField( + model_name='employee', + name='total_invites', + field=models.IntegerField(blank=True, default=0), + ), + migrations.AddField( + model_name='jobcoreinvite', + name='employer', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.Employer'), + ), + migrations.AddField( + model_name='position', + name='status', + field=models.CharField(blank=True, choices=[('ACTIVE', 'Active'), ('DELETED', 'Deleted')], default='ACTIVE', max_length=9), + ), + migrations.AddField( + model_name='profile', + name='employer_role', + field=models.CharField(blank=True, choices=[('ADMIN', 'Admin'), ('SUPERVISOR', 'Supervisor')], default='ADMIN', max_length=25), + ), + migrations.AddField( + model_name='shiftinvite', + name='responded_at', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='venue', + name='status', + field=models.CharField(blank=True, choices=[('ACTIVE', 'Active'), ('DELETED', 'Deleted')], default='ACTIVE', max_length=9), + ), + ] diff --git a/api/mixins.py b/api/mixins.py index 05042b67..4cd12939 100644 --- a/api/mixins.py +++ b/api/mixins.py @@ -21,6 +21,7 @@ def initial(self, request, *args, **kwargs): raise PermissionDenied("You don't seem to be a talent") self.employee = self.request.user.profile.employee + self.user = self.request.user class IsEmployerMixin: @@ -31,6 +32,7 @@ def initial(self, request, *args, **kwargs): raise PermissionDenied("You don't seem to be an employer") self.employer = self.request.user.profile.employer + self.user = self.request.user class WithProfileView(HaveProfileMixin, APIView): diff --git a/api/models.py b/api/models.py index d321250e..f3a1fb20 100644 --- a/api/models.py +++ b/api/models.py @@ -8,11 +8,21 @@ MIDNIGHT = NOW.replace(hour=0, minute=0, second=0) +ACTIVE = 'ACTIVE' +DELETED = 'DELETED' +POSITION_STATUS = ( + (ACTIVE, 'Active'), + (DELETED, 'Deleted'), +) class Position(models.Model): picture = models.URLField(blank=True) title = models.TextField(max_length=100, blank=True) + description = models.TextField(max_length=1050, blank=True) + meta_description = models.TextField(max_length=250, blank=True) + meta_keywords = models.TextField(max_length=250, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) + status = models.CharField(max_length=9,choices=POSITION_STATUS,default=ACTIVE,blank=True) def __str__(self): return self.title @@ -88,8 +98,34 @@ def __str__(self): return self.title +PENDING = 'PENDING' +NOT_APPROVED = 'NOT_APPROVED' +BEING_REVIEWED = 'BEING_REVIEWED' +MISSING_DOCUMENTS = 'MISSING_DOCUMENTS' +APPROVED = 'APPROVED' +EMPLOYEMNT_STATUS = ( + (NOT_APPROVED, 'Not Approved'), + (PENDING, 'Pending'), + (BEING_REVIEWED, 'Being Reviewed'), + (APPROVED, 'Approved'), +) + +SINGLE = 'SINGLE' +MARRIED_JOINTLY = 'MARRIED_JOINTLY' +MARRIED_SEPARATELY = 'MARRIED_SEPARATELY' +HEAD = 'HEAD' +WIDOWER = 'WIDOWER' +FILING_STATUS = ( + (SINGLE, 'Single'), + (MARRIED_JOINTLY, 'Married filing jointly'), + (MARRIED_SEPARATELY, 'Married filing separately'), + (HEAD, 'Head of household'), + (WIDOWER, 'Qualifying widow(er) with dependent child'), +) + + class Employee(models.Model): - response_time = models.IntegerField(blank=True, default=0) # in minutes + user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True) minimum_hourly_rate = models.DecimalField( max_digits=3, decimal_places=1, default=8, blank=True) @@ -105,7 +141,16 @@ class Employee(models.Model): badges = models.ManyToManyField(Badge, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) - document_active = models.BooleanField(default=False) + + #reponse time calculation + response_time = models.IntegerField(blank=True, default=0) # in minutes + total_invites = models.IntegerField(blank=True, default=0) # in minutes + + # employment and deducations + employment_verification_status = models.CharField(max_length=25, choices=EMPLOYEMNT_STATUS, default=NOT_APPROVED,blank=True) + filing_status = models.CharField(max_length=25, choices=FILING_STATUS, default=SINGLE,blank=True) + allowances = models.IntegerField(blank=True, default=0) + extra_withholding = models.FloatField(blank=True, default=0) def __str__(self): return self.user.first_name + " " + self.user.last_name + "(" + self.user.email + ")" @@ -122,6 +167,12 @@ def __str__(self): (PENDING, 'PENDING_EMAIL_VALIDATION'), ) +ADMIN = 'ADMIN' +SUPERVISOR = 'SUPERVISOR' +COMPANY_ROLES = ( + (ADMIN, 'Admin'), + (SUPERVISOR, 'Supervisor'), +) class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, blank=True) @@ -146,15 +197,13 @@ class Profile(models.Model): phone_number = models.CharField(max_length=17, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) - employer = models.ForeignKey( - Employer, on_delete=models.CASCADE, blank=True, null=True) - employee = models.ForeignKey( - Employee, on_delete=models.CASCADE, blank=True, null=True) - status = models.CharField( - max_length=25, - choices=PROFILE_STATUS, - default=PENDING, - blank=True) + + employee = models.ForeignKey(Employee, on_delete=models.CASCADE, blank=True, null=True) + + employer = models.ForeignKey(Employer, on_delete=models.CASCADE, blank=True, null=True) + employer_role = models.CharField(max_length=25,choices=COMPANY_ROLES,default=ADMIN,blank=True) + + status = models.CharField(max_length=25,choices=PROFILE_STATUS,default=PENDING,blank=True) def __str__(self): return self.user.username @@ -205,6 +254,12 @@ def __str__(self): return self.title +ACTIVE = 'ACTIVE' +DELETED = 'DELETED' +VENUE_STATUS = ( + (ACTIVE, 'Active'), + (DELETED, 'Deleted'), +) class Venue(models.Model): title = models.TextField(max_length=100, blank=True) employer = models.ForeignKey( @@ -217,6 +272,7 @@ class Venue(models.Model): zip_code = models.IntegerField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) + status = models.CharField(max_length=9,choices=VENUE_STATUS,default=ACTIVE,blank=True) def __str__(self): return self.title @@ -349,6 +405,7 @@ class ShiftInvite(models.Model): default=PENDING, blank=True) manually_created = models.BooleanField(default=False) + responded_at = models.DateTimeField(blank=False, null=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) @@ -357,13 +414,6 @@ def __str__(self): "%m/%d/%Y, %H:%M:%S") + " (" + self.status + ")" -PENDING = 'PENDING' -ACCEPTED = 'ACCEPTED' -JOBCORE_INVITE_STATUS_CHOICES = ( - (PENDING, 'Pending'), - (ACCEPTED, 'Accepted'), -) - class UserToken(models.Model): token = models.TextField(max_length=255, blank=True) @@ -376,13 +426,22 @@ def __str__(self): return self.email + " " + self.token + +PENDING = 'PENDING' +ACCEPTED = 'ACCEPTED' +JOBCORE_INVITE_STATUS_CHOICES = ( + (PENDING, 'Pending'), + (ACCEPTED, 'Accepted'), +) class JobCoreInvite(models.Model): sender = models.ForeignKey( Profile, on_delete=models.CASCADE, blank=True) first_name = models.TextField(max_length=100, blank=True) last_name = models.TextField(max_length=100, blank=True) - shift = models.ForeignKey( - Shift, on_delete=models.CASCADE, blank=True, default=None, null=True) + + shift = models.ForeignKey(Shift, on_delete=models.CASCADE, blank=True, default=None, null=True) + employer = models.ForeignKey(Employer, on_delete=models.CASCADE, blank=True, default=None, null=True) + email = models.TextField(max_length=100, blank=True) status = models.CharField( max_length=9, @@ -504,7 +563,8 @@ class Clockin(models.Model): default=PENDING) def __str__(self): - return self.employee.user.first_name+" "+self.employee.user.last_name+", from "+str(self.started_at)+" to "+str(self.ended_at) + return self.employee.user.first_name + " " + self.employee.user.last_name + ", from " + str( + self.started_at) + " to " + str(self.ended_at) OPEN = 'OPEN' @@ -612,20 +672,50 @@ class BankAccount(models.Model): stripe_token = models.CharField(max_length=200, null=True, blank=True) +class Document(models.Model): + title = models.CharField(max_length=250, null=True, blank=True) + + validates_identity = models.BooleanField(default=False) + validates_employment = models.BooleanField(default=False) + is_form = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) + + def __str__(self): + return self.title + + class EmployeeDocument(models.Model): PENDING = 'PENDING' APPROVED = 'APPROVED' + ARCHIVED = 'ARCHIVED' + DELETED = 'DELETED' REJECTED = 'REJECTED' DOCUMENT_STATUS = ( (PENDING, 'Pending'), (APPROVED, 'Approved'), + (ARCHIVED, 'Archived'), + (DELETED, 'Deleted'), (REJECTED, 'Rejected'), ) document = models.URLField() - public_id = models.CharField(max_length=30, null=True) - name = models.CharField(max_length=50, null=True, blank=True) + + public_id = models.CharField(max_length=80, null=True) + rejected_reason = models.CharField(max_length=255, null=True) - state = models.CharField(max_length=8, choices=DOCUMENT_STATUS, default=PENDING) + status = models.CharField(max_length=8, choices=DOCUMENT_STATUS, default=PENDING) + expired_at = models.DateTimeField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True, editable=False) updated_at = models.DateTimeField(auto_now=True, editable=False) employee = models.ForeignKey(Employee, null=True, on_delete=models.CASCADE) + document_type = models.ForeignKey(Document, null=True, on_delete=models.CASCADE) + + +class AppVersion(models.Model): + build_number = models.IntegerField(default=94) + version = models.CharField(max_length=10, unique=True, default='94') + change_log = models.TextField(max_length=450, blank=True) + force_update = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True, editable=False) + updated_at = models.DateTimeField(auto_now=True, editable=False) diff --git a/api/serializers/auth_serializer.py b/api/serializers/auth_serializer.py index 5a5e48d1..2fdded98 100644 --- a/api/serializers/auth_serializer.py +++ b/api/serializers/auth_serializer.py @@ -144,6 +144,13 @@ def create(self, validated_data): user.set_password(validated_data['password']) user.save() + #if there is a previous invite as an employer + previous_invite = JobCoreInvite.objects.all().filter(email=user.email, employer__isnull=False).last() + if previous_invite is not None and account_type is None: + account_type = 'employer' + employer = previous_invite.employer + + if account_type == 'employer': Profile.objects.create(user=user, picture='', employer=employer) @@ -175,11 +182,9 @@ def create(self, validated_data): randint(1, 3)) + '.png', employee=emp, status=status, profile_city_id=profile_city, city=city) - jobcore_invites = JobCoreInvite.objects.all().filter( - email=user.email) + jobcore_invites = JobCoreInvite.objects.all().filter(email=user.email, employer__isnull=True) - auth_actions.create_shift_invites_from_jobcore_invites( - jobcore_invites, user.profile.employee) + auth_actions.create_shift_invites_from_jobcore_invites(jobcore_invites, user.profile.employee) jobcore_invites.update(status='ACCEPTED') diff --git a/api/serializers/documents_serializer.py b/api/serializers/documents_serializer.py index 2afc7fca..9364d0d8 100644 --- a/api/serializers/documents_serializer.py +++ b/api/serializers/documents_serializer.py @@ -1,9 +1,79 @@ from rest_framework import serializers -from api.models import EmployeeDocument +from api.models import EmployeeDocument, Document + + +class DocumentSerializer(serializers.ModelSerializer): + class Meta: + model = Document + exclude = () class EmployeeDocumentSerializer(serializers.ModelSerializer): class Meta: model = EmployeeDocument exclude = () + + def create(self, validated_data): + + validates_employment = validated_data['document_type'].validates_employment + validates_identity = validated_data['document_type'].validates_identity + is_form = validated_data['document_type'].is_form + + if validates_employment: + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, + status__in=['PENDING', 'REJECTED'], + document_type__validates_employment=True).update(status='DELETED') + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, status='APPROVED', + document_type__validates_employment=True).update(status='ARCHIVED') + + if validates_identity: + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, + status__in=['PENDING', 'REJECTED'], + document_type__validates_identity=True).update(status='DELETED') + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, status='APPROVED', + document_type__validates_identity=True).update(status='ARCHIVED') + + if is_form: + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, + status__in=['PENDING', 'REJECTED'], document_type__is_form=True).update( + status='DELETED') + EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, status='APPROVED', + document_type__is_form=True).update(status='ARCHIVED') + + new_document = EmployeeDocument(**validated_data) + new_document.save() + + # pick the new status for the employee + pending_and_approved_documents = EmployeeDocument.objects.filter(employee__id=validated_data['employee'].id, + status__in=['PENDING', 'APPROVED']) + validations = {"employment": False, "identity": False, "form": False} + # you need to have one of each type at least + for doc in pending_and_approved_documents: + if doc.document_type.validates_employment: + validations["employment"] = True + if doc.document_type.validates_identity: + validations["identity"] = True + if doc.document_type.is_form: + validations["form"] = True + + employee = new_document.employee + if validations["form"] and validations["identity"] and validations["employment"]: + employee.employment_verification_status = 'BEING_REVIEWED' + else: + employee.employment_verification_status = 'MISSING_DOCUMENTS' + employee.save() + + return new_document + + +class EmployeeDocumentGetSerializer(serializers.ModelSerializer): + document_type = DocumentSerializer(many=False) + name = serializers.SerializerMethodField('_name', read_only=True) + + def _name(self, object): + return object.document_type.title + + class Meta: + model = EmployeeDocument + exclude = () diff --git a/api/serializers/other_serializer.py b/api/serializers/other_serializer.py index 5ec97feb..afb58ce2 100644 --- a/api/serializers/other_serializer.py +++ b/api/serializers/other_serializer.py @@ -2,14 +2,20 @@ from api.serializers import profile_serializer from api.utils import notifier from api.models import ( - Badge, JobCoreInvite, Rate, Employer, Profile, - Shift, Employee, User, AvailabilityBlock, City, - # Document + Badge, JobCoreInvite, Rate, Employer, Profile, + Shift, Employee, User, AvailabilityBlock, City, + AppVersion, ) from api.serializers.position_serializer import PositionSmallSerializer +class AppVersionSerializer(serializers.ModelSerializer): + class Meta: + model = AppVersion + exclude = () + + class EmployerGetSmallSerializer(serializers.ModelSerializer): class Meta: model = Employer @@ -95,8 +101,7 @@ def validate(self, data): if user is not None: profile = Profile.objects.filter(user=user).first() if profile is not None: - raise serializers.ValidationError( - "The user is already registered in jobcore") + raise serializers.ValidationError("The user is already registered in jobcore") try: sender = self.context['request'].user.profile.id @@ -204,20 +209,22 @@ def validate(self, data): # django_end_week_day = (start.isoweekday() % 7) + 1 if data['recurrency_type'] == 'WEEKLY': - + # TODO: Duplicated code with line 273 previous_ablock_in_week = AvailabilityBlock.objects.filter( starting_at__week_day=django_start_week_day, recurrency_type='WEEKLY', employee_id=self.context['request'].user.profile.id ) - #if updating + # if updating if self.instance: previous_ablock_in_week = previous_ablock_in_week.exclude( id=self.instance.id) previous_ablock_in_week = previous_ablock_in_week.count() if previous_ablock_in_week > 0: - raise serializers.ValidationError('This employee has '+str(previous_ablock_in_week)+' day block(s) for '+days[str(django_start_week_day)]+' already') # NOQA + raise serializers.ValidationError( + 'This employee has ' + str(previous_ablock_in_week) + ' day block(s) for ' + days[ + str(django_start_week_day)] + ' already') # NOQA return data @@ -270,7 +277,7 @@ def validate(self, data): # django_end_week_day = (start.isoweekday() % 7) + 1 if data['recurrency_type'] == 'WEEKLY': - + # TODO: Duplicated code with line 207 previous_ablock_in_week = AvailabilityBlock.objects.filter( starting_at__week_day=django_start_week_day, recurrency_type='WEEKLY', employee_id=self.context['request'].user.profile.id @@ -288,4 +295,3 @@ def validate(self, data): str(django_start_week_day)] + ' already') # NOQA return data - diff --git a/api/serializers/payment_serializer.py b/api/serializers/payment_serializer.py index af3cb14a..faf832a9 100644 --- a/api/serializers/payment_serializer.py +++ b/api/serializers/payment_serializer.py @@ -191,8 +191,11 @@ def validate(self, data): data = super(PayrollPeriodPaymentSerializer, self).validate(data) + if data['payroll_period'].status != 'OPEN': + raise serializers.ValidationError('This payroll period is not open for changes anymore') + if 'status' not in data: - raise serializers.ValidationError('You need to specify the shift status') + raise serializers.ValidationError('You need to specify the status') return data diff --git a/api/serializers/user_serializer.py b/api/serializers/user_serializer.py index 1380d6b7..cfb25758 100644 --- a/api/serializers/user_serializer.py +++ b/api/serializers/user_serializer.py @@ -5,7 +5,7 @@ class ProfileGetSmallSerializer(serializers.ModelSerializer): class Meta: model = Profile - fields = ('picture', 'id', 'bio', 'status', 'employer', 'employee') + fields = ('picture', 'id', 'bio', 'status', 'employer', 'employer_role', 'employee') class UserGetTinySerializer(serializers.ModelSerializer): diff --git a/api/tests/test_app_version.py b/api/tests/test_app_version.py new file mode 100644 index 00000000..2cf7b560 --- /dev/null +++ b/api/tests/test_app_version.py @@ -0,0 +1,32 @@ +from django.test import TestCase + +from django.urls import reverse_lazy + +from api.models import AppVersion +from api.tests.mixins import WithMakeUser + + +class AppVersionTestSuite(TestCase, WithMakeUser): + + def setUp(self): + ( + self.test_user_employee, + self.test_employee, + self.test_profile_employee + ) = self._make_user( + 'employee', + userkwargs=dict( + username='employee1234', + email='employee1234@testdoma.in', + is_active=True, + ) + ) + + def test_get(self): + AppVersion.objects.create(version=10) + url = reverse_lazy('api:single-version', kwargs=dict(version='last')) + self.client.force_login(self.test_user_employee) + response = self.client.get(url, content_type='application/json') + json_response = response.json() + self.assertEquals(response.status_code, 200, response.content) + self.assertEquals(json_response.get("build_number"), 94, response.content) diff --git a/api/tests/test_employee_documents.py b/api/tests/test_employee_documents.py index af979236..350fe57d 100644 --- a/api/tests/test_employee_documents.py +++ b/api/tests/test_employee_documents.py @@ -4,6 +4,7 @@ from mock import patch from io import BytesIO from django.test.client import MULTIPART_CONTENT +from mixer.backend.django import mixer from api.models import EmployeeDocument, Employee from api.tests.mixins import WithMakeUser @@ -34,23 +35,26 @@ def test_add_document(self, mocked_uploader): self.client.force_login(self.test_user_employee) with BytesIO(b'the-data') as f: + _type = mixer.blend('api.Document') payload = { 'document': f, + 'document_type': _type.id } # payload = self.client._encode_data(payload, MULTIPART_CONTENT) response = self.client.post(url, payload, content_type=MULTIPART_CONTENT) - print(response.content) + self.assertEquals( response.status_code, 201, f'It should return a success response: {str(response.content)}') @patch('cloudinary.uploader.upload') - def test_get_my_documents(self, mocked_uploader): + def test_get(self, mocked_uploader): + document = { 'document': 'http://a-valid.url/for-the-doc', "employee_id": Employee.objects.all()[0].id, - "name": "Test Name" + "document_type": mixer.blend('api.Document') } EmployeeDocument.objects.create(**document) diff --git a/api/urls.py b/api/urls.py index df47b662..da3d8e9a 100644 --- a/api/urls.py +++ b/api/urls.py @@ -12,7 +12,8 @@ PasswordView, ValidateEmailView, UserView, UserRegisterView, EmployeeView, EmployerView, ProfileMeView, ProfileMeImageView, JobCoreInviteView, CatalogView, RateView, BadgeView, PayrollShiftsView, ProjectedPaymentsView, - PositionView, OnboardingView, ValidateSendEmailView, CityView, PublicShiftView + PositionView, OnboardingView, ValidateSendEmailView, CityView, PublicShiftView, + AppVersionView ) from api.views.bank_accounts_view import BankAccountAPIView, BankAccountDetailAPIView @@ -27,7 +28,7 @@ ) from api.views.documents_view import ( - EmployeeDocumentAPI, EmployeeDocumentDetailAPI + EmployeeDocumentAPI, EmployeeDocumentDetailAPI, DocumentAPI ) from api.views.employer_views import ( @@ -46,7 +47,9 @@ # # PUBLIC ENDPOINTS # - + path('version/', AppVersionView.as_view(), name="single-version"), + path('version', AppVersionView.as_view(), name="version"), + path('login', ObtainJSONWebToken.as_view( serializer_class=CustomJWTSerializer)), path('user', include('django.contrib.auth.urls'), name="user-auth"), @@ -149,10 +152,7 @@ 'employers/me/image', EmployerMeImageView.as_view(), name="me-employers-image"), - path( - 'employers/me/users', - EmployerMeUsersView.as_view(), - name="me-employer-users"), + path('employers/me/users', EmployerMeUsersView.as_view(),name="me-employer-users"), path( 'employers/me/applications', ApplicantsView.as_view(), @@ -340,6 +340,11 @@ path('employees/me/payroll-payments', EmployeeMePayrollPaymentsView.as_view(), name="me-get-payroll-payments"), + # DOCUMENTS + path('documents', DocumentAPI.as_view(), name="document"), + path('employees/me/documents/', EmployeeDocumentDetailAPI.as_view(), name="employee-document-detail"), + path('employees/me/documents', EmployeeDocumentAPI.as_view(), name="employee-document"), + # # ADMIN USE ONLY # @@ -349,27 +354,18 @@ EmployeeBadgesView.as_view(), name="admin-id-employees-badges"), # update the talent badges - path( - 'positions', - PositionView.as_view(), - name="admin-get-positions"), + path('positions',PositionView.as_view(),name="admin-get-positions"), path( 'positions/', PositionView.as_view(), name="admin-id-positions"), - path( - 'periods', - PayrollPeriodView.as_view(), - name="admin-get-periods"), + path('periods', PayrollPeriodView.as_view(), name="admin-get-periods"), path( 'periods/', PayrollPeriodView.as_view(), name="admin-get-periods"), path('bank-accounts/', BankAccountAPIView.as_view(), name='api-bank-accounts'), path('bank-accounts/', BankAccountDetailAPIView.as_view(), name='detail-api-bank-accounts'), - # DOCUMENTS - path('document/', EmployeeDocumentDetailAPI.as_view(), name="employee-document-detail"), - path('document/', EmployeeDocumentAPI.as_view(), name="employee-document"), ### @@ -391,6 +387,4 @@ # - employer: optional path('hook/generate_periods', GeneratePeriodsView.as_view()), - # path('employees/me/documents', EmployeeMeDocumentView.as_view(), name="me-documents"), - # path('employees/me/documents/', EmployeeMeDocumentView.as_view(), name="me-documents"), ] diff --git a/api/views/documents_view.py b/api/views/documents_view.py index 6594aec0..9569d3ba 100644 --- a/api/views/documents_view.py +++ b/api/views/documents_view.py @@ -18,48 +18,101 @@ class EmployeeDocumentAPI(EmployeeView): def post(self, request): + + if 'document_type' not in request.data: + return Response(validators.error_object('You need to specify the type of document you are uploading'), + status=status.HTTP_400_BAD_REQUEST) + if 'document' not in request.FILES: - return Response( - validators.error_object('No Document'), - status=status.HTTP_400_BAD_REQUEST) + return Response(validators.error_object('Please specify a document'), status=status.HTTP_400_BAD_REQUEST) - file_name = f'i9_documents/profile-{str(self.request.user.id)}-{datetime.now().strftime("%d-%m")}-{get_random_string(length=32)}' + public_id = f'profile-{str(self.request.user.id)}-{datetime.now().strftime("%d-%m")}-{get_random_string(length=32)}' + file_name = f'{str(self.request.user.id)}/i9_documents/{public_id}' try: result = cloudinary.uploader.upload( request.FILES['document'], public_id=file_name, - tags=['i9_document'], + tags=['i9_document', 'profile-' + str(self.request.user.id), ], use_filename=1, unique_filename=1, resource_type='auto' ) except Exception as e: return JsonResponse({"error": str(e)}, status=400) - request.data['document'] = result['secure_url'] - request.data['employee'] = self.employee.id - log.debug(f"EmployeeDocumentAPI:post:{str(request.data)}") - print(f"EmployeeDocumentAPI:post:{str(request.data)}") - print(f"EmployeeDocumentAPI:post:{str(request.data)}") - serializer = documents_serializer.EmployeeDocumentSerializer(data=request.data) + + data = { + 'document': result['secure_url'], + 'employee': self.employee.id, + 'document_type': request.data['document_type'], + 'public_id': public_id, + } + + serializer = documents_serializer.EmployeeDocumentSerializer(data=data) if serializer.is_valid(): serializer.save() return Response(serializer.data, status=status.HTTP_201_CREATED) + + cloudinary.uploader.destroy(public_id) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def get(self, request): + documents = EmployeeDocument.objects.filter(employee_id=self.employee.id) - data = documents_serializer.EmployeeDocumentSerializer(documents, many=True).data - return JsonResponse(data, status=200, safe=False) + + qStatus = request.GET.get('status') + if qStatus: + documents = documents.filter(status=qStatus) + else: + # by default, archived documents will be hidden + documents = documents.filter(status__in=['PENDING', 'APPROVED', 'REJECTED']) + + qType = request.GET.get('type') + if qType: + if qType == 'identity': + documents = documents.filter(document_type__validates_identity=True) + elif qType == 'employment': + documents = documents.filter(document_type__validates_employment=True) + elif qType == 'form': + documents = documents.filter(document_type__is_form=True) + + qTypeId = request.GET.get('type_id') + if qTypeId: + documents = documents.filter(document_type=qTypeId) + + serializer = documents_serializer.EmployeeDocumentGetSerializer(documents, many=True) + return JsonResponse(serializer.data, status=200, safe=False) class EmployeeDocumentDetailAPI(EmployeeView): def delete(self, request, document_id): try: document = EmployeeDocument.objects.get(id=document_id) + + cloudinary.uploader.destroy(document.public_id) + + document.delete() except EmployeeDocument.DoesNotExist: return Response(validators.error_object( 'Not found.'), status=status.HTTP_404_NOT_FOUND) - document.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + +class DocumentAPI(EmployeeView): + + def get(self, request): + + documents = Document.objects.all() + + qType = request.GET.get('type') + if qType: + if qType == 'identity': + documents = documents.filter(validates_identity=True) + elif qType == 'employment': + documents = documents.filter(validates_employment=True) + elif qType == 'form': + documents = documents.filter(is_form=True) + + serializer = documents_serializer.DocumentSerializer(documents, many=True) + return JsonResponse(serializer.data, status=200, safe=False) diff --git a/api/views/employer_views.py b/api/views/employer_views.py index 17503c1c..83064c62 100644 --- a/api/views/employer_views.py +++ b/api/views/employer_views.py @@ -22,7 +22,8 @@ from api.serializers import ( employer_serializer, user_serializer, shift_serializer, payment_serializer, venue_serializer, favlist_serializer, - employee_serializer, clockin_serializer, rating_serializer + employee_serializer, clockin_serializer, rating_serializer, + profile_serializer ) from django.utils import timezone @@ -101,6 +102,30 @@ def get(self, request, id=False): serializer = user_serializer.UserGetSmallSerializer(qs, many=many) return Response(serializer.data, status=status.HTTP_200_OK) + def put(self, request, id): + + try: + user = self.get_queryset().get(profile__id=id) + except User.DoesNotExist: + return Response(validators.error_object('Not found.'), status=status.HTTP_404_NOT_FOUND) + + serializer = profile_serializer.ProfileSerializer(user.profile, data=request.data, context={"request": request}) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, id): + + qs = self.get_queryset() + try: + qs = qs.get(profile__id=id) + + return Response(status=status.HTTP_204_NO_CONTENT) + + except User.DoesNotExist: + return Response(validators.error_object('Not found.'), status=status.HTTP_404_NOT_FOUND) + class ApplicantsView(EmployerView): def get_queryset(self): @@ -272,6 +297,8 @@ def get(self, request, id=False): except Venue.DoesNotExist: return Response(validators.error_object( 'Not found.'), status=status.HTTP_404_NOT_FOUND) + else: + qs = qs.filter(status='ACTIVE') serializer = venue_serializer.VenueSerializer(qs, many=many) return Response(serializer.data, status=status.HTTP_200_OK) @@ -311,10 +338,15 @@ def delete(self, request, id): try: venue = self.get_queryset().get(id=id) except Venue.DoesNotExist: - return Response(validators.error_object( - 'Not found.'), status=status.HTTP_404_NOT_FOUND) - - venue.delete() + return Response(validators.error_object('Not found.'), status=status.HTTP_404_NOT_FOUND) + + count = venue.shift_set.count() + if count == 0: + venue.delete() + else: + venue.status = "DELETED" + venue.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/api/views/general_views.py b/api/views/general_views.py index 30721470..9f887d3b 100644 --- a/api/views/general_views.py +++ b/api/views/general_views.py @@ -24,6 +24,8 @@ from rest_framework.views import APIView from rest_framework_jwt.settings import api_settings +from django.http import JsonResponse + import api.utils.jwt from api.pagination import HeaderLimitOffsetPagination @@ -377,7 +379,7 @@ def put(self, request): result = cloudinary.uploader.upload( request.FILES['image'], - public_id='profile' + str(profile.id), + public_id=f'{str(profile.id)}/profile' + str(profile.id), crop='limit', width=450, height=450, @@ -387,7 +389,7 @@ def put(self, request): 'radius': 100 }, ], - tags=['profile_picture'] + tags=['profile_picture', 'profile' + str(profile.id)] ) profile.picture = result['secure_url'] @@ -409,7 +411,7 @@ def get(self, request, id=False): serializer = position_serializer.PositionSerializer( position, many=False) else: - positions = Position.objects.all() + positions = Position.objects.filter(status='ACTIVE') serializer = position_serializer.PositionSerializer( positions, many=True) @@ -443,7 +445,12 @@ def delete(self, request, id): return Response(validators.error_object( 'Not found.'), status=status.HTTP_404_NOT_FOUND) - position.delete() + if position.shift_set.count() > 0 or position.employee_set.count() > 0: + position.status = 'DELETED' + position.save() + else: + position.delete() + return Response(status=status.HTTP_204_NO_CONTENT) @@ -965,7 +972,8 @@ def get(self, request): TODAY = datetime.datetime.now(tz=timezone.utc) - shifts = Shift.objects.annotate(num_employees=Count('employees')).filter(num_employees__lt=F('maximum_allowed_employees')) + shifts = Shift.objects.annotate(num_employees=Count('employees')).filter( + num_employees__lt=F('maximum_allowed_employees')) qStatus = request.GET.get('status') if validators.in_choices(qStatus, SHIFT_STATUS_CHOICES): @@ -997,7 +1005,7 @@ def get(self, request): if qEnd is not None and qEnd != '': end = timezone.make_aware(datetime.datetime.strptime(qEnd, DATE_FORMAT)) shifts = shifts.filter(ending_at__lte=end) - + shifts = shifts.order_by('-starting_at') paginator = HeaderLimitOffsetPagination() @@ -1006,4 +1014,27 @@ def get(self, request): serializer = shift_serializer.ShiftGetPublicTinySerializer(page, many=True) return paginator.get_paginated_response(serializer.data) else: - return Response([], status=status.HTTP_200_OK) \ No newline at end of file + return Response([], status=status.HTTP_200_OK) + + +class AppVersionView(APIView): + permission_classes = (AllowAny,) + + def get(self, request, version=None): + + if version: + try: + if version == 'last': + app_version = AppVersion.objects.last() + else: + app_version = AppVersion.objects.get(version=version) + + serializer = other_serializer.AppVersionSerializer(app_version, many=False) + return Response(serializer.data, status=status.HTTP_200_OK) + + except AppVersion.DoesNotExist: + return Response({"detail": "The app version was not found"}, status=status.HTTP_404_NOT_FOUND) + + versions = AppVersion.objects.all() + serializer = other_serializer.AppVersionSerializer(versions, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/api/views/hooks.py b/api/views/hooks.py index fa3f564d..a00d600f 100644 --- a/api/views/hooks.py +++ b/api/views/hooks.py @@ -10,7 +10,7 @@ from django.contrib.auth.models import User from api.models import (Employee, Shift, ShiftInvite, ShiftApplication, Clockin, Employer, AvailabilityBlock, FavoriteList, Venue, JobCoreInvite, - Rate, FCMDevice, Notification, PayrollPeriod, PayrollPeriodPayment, Profile, Position) + Rate, FCMDevice, Notification, PayrollPeriod, PayrollPeriodPayment, Profile, Position, EmployeeDocument) from api.actions import employee_actions from api.serializers import clockin_serializer, payment_serializer, shift_serializer @@ -128,4 +128,19 @@ def process_expired_shifts(): # delete applications for expired shifts ShiftApplication.objects.filter(shift__status='EXPIRED').delete() - return True \ No newline at end of file + return True + + +def process_expired_documents(): + + NOW = utc.localize(datetime.now()) + # if now > shift.ending_at + delta: + archived_documents = EmployeeDocument.objects.filter(expired_at__isnull=False, expired_at__lte= NOW).update(status='ARCHIVED') + + #delete documents from cloudnary + deleted_documents = EmployeeDocument.objects.filter(status='DELETED') + for doc in deleted_documents: + cloudinary.uploader.destroy(doc.public_id) + doc.delete() + + return archived_documents \ No newline at end of file diff --git a/docs/documents.md b/docs/documents.md index dd40f692..734288c8 100644 --- a/docs/documents.md +++ b/docs/documents.md @@ -1,43 +1,33 @@ # Documents -## Create a new Document +0. Crear un documento -**URL** : `/api/document` - -**Method** : `POST` +POST /employee/me/documents **Auth required** : YES **Permissions required** : Authenticated User -## Request Params - +**Parameters**: | key | Example Value | Required? | | -------------------- | ------------ | ------------- | -| document | "" | Yes | - +| document | | Yes | +| document_type | 1 | Yes | -## Example: -http://localhost:5000/api/document ## Success Response **Code** : `200 OK` -**Content examples** - - -```json - -``` ## Notes -## List my documents -**URL** : `/api/document` +1. Para obtener todos los tipos de documentos de una lista: + +**URL** : `/documents?type=` **Method** : `GET` @@ -45,9 +35,9 @@ http://localhost:5000/api/document **Permissions required** : Authenticated User -## Example: +**Querystring**: + - type=identity,employment,form -http://localhost:5000/api/document ## Success Response @@ -68,4 +58,27 @@ http://localhost:5000/api/document ] ``` -## Notes \ No newline at end of file +## Notes + + +``` +2. Para obtener todos los documentos del employee que esta logeado: +``` +GET /employee/me/documents?type=&status=&type_id= + +Querystring: + - type=identity,employment,form + - status=PENDING,APPROVED,REJECTED + - type_id=id del tipo, por ejemplo: 1,2,3,etc. + +``` + +3. Para borrar un document +``` +DELETE /employee/me/documents/id +``` + +4. Para obtener detalles de un documento +``` +GET /employee/me/documents/id +``` \ No newline at end of file diff --git a/docs/profile.md b/docs/profile.md index d80146b2..7032dad8 100644 --- a/docs/profile.md +++ b/docs/profile.md @@ -3,22 +3,111 @@ -PUT /api/profiles/me +## PUT /api/profiles/me ### REQUEST -``` +```json { - 'first_name': 'Angel', - 'last_name': 'Cerberus', - 'bio': 'Some BIO', - "city": "Miami", - "profile_city": - } + "first_name": "Angel", + "last_name": "Cerberus", + "bio": "Some BIO", + "city": "Miami", + "profile_city": "" +} ``` ### Response +```json +{ + "id":6, + "user":{ + "first_name":"Angel", + "last_name":"Cerberus", + "email":"employee1@testdoma.in" + }, + "picture":"https://scot...d_at", + "created_at": "2019-12-15T15:40:53.502453Z", + "status":"PENDING_EMAIL_VALIDATION", + "profile_city":7, + "employer":null, + "employee":6 +} ``` -{"id":6,"user":{"first_name":"Angel","last_name":"Cerberus","email":"employee1@testdoma.in"},"picture":"https://scot...d_at":"2019-12-15T15:40:53.502453Z","status":"PENDING_EMAIL_VALIDATION","profile_city":7,"employer":null,"employee":6} + +## GET /api/profiles/me +```json +{ + "id": 2, + "positions": [ + { + "id": 1, + "picture": "", + "title": "Server", + "description": "", + "meta_description": "", + "meta_keywords": "", + "created_at": "2018-09-13T19:45:00Z", + "updated_at": "2018-09-13T19:45:00Z" + }, + { + "id": 3, + "picture": "", + "title": "Floor Manager", + "description": "", + "meta_description": "", + "meta_keywords": "", + "created_at": "2018-09-13T19:45:00Z", + "updated_at": "2018-09-13T19:45:00Z" + } + ], + "badges": [ + { + "title": "English Proficient", + "id": 1 + }, + { + "title": "Service Quality", + "id": 2 + } + ], + "favoritelist_set": [ + { + "id": 1, + "title": "Preferred Employees", + "created_at": "2018-09-13T19:45:00Z", + "updated_at": "2018-09-13T19:45:00Z", + "auto_accept_employees_on_this_list": true, + "employer": 1, + "employees": [ + 3, + 2 + ] + } + ], + "user": { + "first_name": "John", + "last_name": "Lennon", + "email": "a+employee2@jobcore.co", + "profile": { + "picture": "", + "bio": "" + } + }, + "response_time": 0, + "minimum_hourly_rate": "8.0", + "stop_receiving_invites": false, + "rating": null, + "total_ratings": 0, + "total_pending_payments": 0, + "maximum_job_distance_miles": 50, + "job_count": 0, + "created_at": "2018-09-13T19:45:00Z", + "updated_at": "2019-12-18T18:24:31.768428Z", + "employment_verification_status": "MISSING_DOCUMENTS", + "filing_status": "SINGLE", + "allowances": 0, + "extra_withholding": 0.0 +} ``` \ No newline at end of file diff --git a/docs/version.md b/docs/version.md new file mode 100644 index 00000000..2a270c15 --- /dev/null +++ b/docs/version.md @@ -0,0 +1,12 @@ + +# Get App Version + +GET /api/version + +### Response + +``` +{ + "version": 94 +} +``` \ No newline at end of file