class DeveloperApplication(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=30) email = models.EmailField(unique=True, validators=[validate_email]) phone_number = models.CharField(max_length=15) country = CountryField() city = models.CharField(max_length=50) stack = models.TextField() experience = models.TextField() discovery_story = models.TextField() status = models.PositiveSmallIntegerField( choices=APPLICATION_STATUS_CHOICES, help_text=','.join(['%s - %s' % (item[0], item[1]) for item in APPLICATION_STATUS_CHOICES]), default=REQUEST_STATUS_INITIAL ) created_at = models.DateTimeField(auto_now_add=True) confirmation_key = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) confirmation_sent_at = models.DateTimeField(blank=True, null=True, editable=False) used = models.BooleanField(default=False) used_at = models.DateTimeField(blank=True, null=True, editable=False) def __str__(self): return self.display_name @property def display_name(self): return '%s %s' % (self.first_name, self.last_name) @property def country_name(self): return self.country.name country_name.fget.short_description = 'country'
class TaskRequest(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING) task = models.ForeignKey(Task, on_delete=models.CASCADE) type = models.PositiveSmallIntegerField( choices=TASK_REQUEST_CHOICES, help_text=','.join( ['%s - %s' % (item[0], item[1]) for item in TASK_REQUEST_CHOICES])) created_at = models.DateTimeField(auto_now_add=True) def __unicode__(self): return '%s - %s' % (self.get_type_display(), self.task.summary) @staticmethod @allow_staff_or_superuser def has_read_permission(request): return True @allow_staff_or_superuser def has_object_read_permission(self, request): return self.task.has_object_read_permission(request) @staticmethod @allow_staff_or_superuser def has_write_permission(request): return request.user.type == USER_TYPE_DEVELOPER @allow_staff_or_superuser def has_object_write_permission(self, request): return request.user == self.user
class BaseAttachmentModel(BaseModel): """ Base model to store files or images to Items """ name = models.CharField( null=True, blank=True, max_length=255, verbose_name=_('BaseItemAttachmentModel.name.verbose_name'), help_text=_('BaseItemAttachmentModel.name.help_text')) position = models.PositiveSmallIntegerField( # Note: Will be set in admin via adminsortable2 # The JavaScript which performs the sorting is 1-indexed ! default=0, blank=False, null=False) def __str__(self): return self.name def full_clean(self, *, parent_instance, **kwargs): if self.user_id is None: # inherit owner of this link from parent model instance self.user_id = parent_instance.user_id return super().full_clean(**kwargs) class Meta: abstract = True
class Questionnaire(models.Model): PHASE_CHOICES = ( (1, 'Phase-1'), (2, 'Phase-2'), (3, 'Phase-3'), (4, 'Phase-4'), ) identifier = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) name = models.CharField(max_length=500) phase = models.PositiveSmallIntegerField(choices=PHASE_CHOICES, default=1) root = models.BooleanField( default=False, help_text='Note that you cannot delete the root questionnaire.') def __str__(self): return self.name # TODO needed? (already enforced in forms.py) def save(self, force_insert=False, force_update=False, using=None, update_fields=None, *args, **kwargs): root_field = self.root if Questionnaire.objects.filter(root=True).exists(): invalid = False if self.pk is None: # if new questionnaire is being added if root_field is True: invalid = True else: # if existing questionnaire is being edited if Questionnaire.objects.get(root=True).pk != self.pk: if root_field is True: invalid = True if invalid: raise ValidationError('You already have a root questionnaire!') super(Questionnaire, self).save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields, *args, **kwargs) # TODO needed? (already enforced in admin.py) def delete(self, using=None, keep_parents=False): if self.root: raise ValidationError( 'You cannot delete a root question! Consider editing it instead.' ) super(Questionnaire, self).delete(using=using, keep_parents=keep_parents)
class Post(models.Model): POST_TYPE = ( (1, 'Article'), (2, 'Course'), (3, 'Job'), (4, 'Project'), ) identifier = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) slug = models.SlugField(unique=True, null=True, blank=True, max_length=512) type = models.PositiveSmallIntegerField(choices=POST_TYPE, default=1) tags = tagulous.models.TagField(related_name='posts', to=Tag, blank=True) author = models.ForeignKey('users.CustomUser', on_delete=models.CASCADE, related_name='blogs') title = models.CharField(max_length=255) body = BleachField() preview = models.CharField( max_length=300, help_text='A short preview of this post that is shown in list of posts.' ) likes = models.ManyToManyField('users.CustomUser', blank=True) allow_comments = models.BooleanField(default=True) def save(self, *args, **kwargs): self.slug = slugify(f'{self.title} {self.identifier}', allow_unicode=True) super(Post, self).save(*args, **kwargs) def __str__(self): return f'{self.title}, by {self.author.first_name} {self.author.last_name}' @property def likes_count(self): return self.likes.count() @property def relative_url(self): return reverse('blog-post', kwargs={'slug': self.slug}) def get_absolute_url(self): domain = Site.objects.get_current().domain protocol = "https" if settings.PRODUCTION_SERVER else "http" absolute_url = f'{protocol}://{domain}{self.relative_url}' return absolute_url
class Question(models.Model): identifier = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) body = models.CharField(max_length=1000) questionnaire = models.ForeignKey('Questionnaire', related_name='question', on_delete=models.CASCADE) multiselect = models.BooleanField(default=False) position = models.PositiveSmallIntegerField("position", null=True) class Meta: ordering = ['position'] def __str__(self): return self.body
class ProgressEvent(models.Model): task = models.ForeignKey(Task, on_delete=models.CASCADE) type = models.PositiveSmallIntegerField( choices=PROGRESS_EVENT_TYPE_CHOICES, default=PROGRESS_EVENT_TYPE_DEFAULT, help_text=','.join([ '%s - %s' % (item[0], item[1]) for item in PROGRESS_EVENT_TYPE_CHOICES ])) due_at = models.DateTimeField() title = models.CharField(max_length=200, blank=True, null=True) description = models.CharField(max_length=1000, blank=True, null=True) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='progress_events_created', blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) last_reminder_at = models.DateTimeField(blank=True, null=True) def __unicode__(self): return '%s | %s - %s' % (self.get_type_display(), self.task.summary, self.due_at) class Meta: unique_together = ('task', 'due_at') ordering = ['due_at'] @staticmethod @allow_staff_or_superuser def has_read_permission(request): return True @allow_staff_or_superuser def has_object_read_permission(self, request): return self.task.has_object_read_permission(request) @staticmethod @allow_staff_or_superuser def has_write_permission(request): return request.user.type == USER_TYPE_PROJECT_OWNER @allow_staff_or_superuser def has_object_write_permission(self, request): return request.user == self.task.user
class ProgressReport(models.Model): event = models.OneToOneField(ProgressEvent, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING) status = models.PositiveSmallIntegerField( choices=PROGRESS_REPORT_STATUS_CHOICES, help_text=','.join([ '%s - %s' % (item[0], item[1]) for item in PROGRESS_REPORT_STATUS_CHOICES ])) percentage = models.PositiveIntegerField( validators=[MinValueValidator(0), MaxValueValidator(100)]) accomplished = models.TextField() next_steps = models.TextField(blank=True, null=True) remarks = models.TextField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) uploads = GenericRelation(Upload, related_query_name='progress_reports') def __unicode__(self): return '{0} - {1}%'.format(self.event, self.percentage) @staticmethod @allow_staff_or_superuser def has_read_permission(request): return True @allow_staff_or_superuser def has_object_read_permission(self, request): return self.event.task.has_object_read_permission(request) @staticmethod @allow_staff_or_superuser def has_write_permission(request): return request.user.type == USER_TYPE_DEVELOPER @allow_staff_or_superuser def has_object_write_permission(self, request): return request.user == self.user
class Task(models.Model): project = models.ForeignKey(Project, related_name='tasks', on_delete=models.DO_NOTHING, blank=True, null=True) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='tasks_created', on_delete=models.DO_NOTHING) title = models.CharField(max_length=200) description = models.TextField(blank=True, null=True) remarks = models.TextField(blank=True, null=True) url = models.URLField(blank=True, null=True) fee = models.BigIntegerField() currency = models.CharField(max_length=5, choices=CURRENCY_CHOICES, default=CURRENCY_CHOICES[0][0]) deadline = models.DateTimeField(blank=True, null=True) skills = tagulous.models.TagField(Skill, blank=True) visibility = models.PositiveSmallIntegerField( choices=VISIBILITY_CHOICES, default=VISIBILITY_CHOICES[0][0]) update_interval = models.PositiveIntegerField(blank=True, null=True) update_interval_units = models.PositiveSmallIntegerField( choices=UPDATE_SCHEDULE_CHOICES, blank=True, null=True) apply = models.BooleanField(default=True) closed = models.BooleanField(default=False) paid = models.BooleanField(default=False) applicants = models.ManyToManyField(settings.AUTH_USER_MODEL, through='Application', through_fields=('task', 'user'), related_name='task_applications', blank=True) participants = models.ManyToManyField(settings.AUTH_USER_MODEL, through='Participation', through_fields=('task', 'user'), related_name='task_participants', blank=True) satisfaction = models.SmallIntegerField(blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) apply_closed_at = models.DateTimeField(blank=True, null=True) closed_at = models.DateTimeField(blank=True, null=True) paid_at = models.DateTimeField(blank=True, null=True) comments = GenericRelation(Comment, related_query_name='tasks') uploads = GenericRelation(Upload, related_query_name='tasks') ratings = GenericRelation(Rating, related_query_name='tasks') def __unicode__(self): return self.summary class Meta: ordering = ['-created_at'] unique_together = ('user', 'title', 'fee') @staticmethod @allow_staff_or_superuser def has_read_permission(request): return True @allow_staff_or_superuser def has_object_read_permission(self, request): if self.visibility == VISIBILITY_DEVELOPER: return request.user.type == USER_TYPE_DEVELOPER elif self.visibility == VISIBILITY_MY_TEAM: return bool( Connection.objects.exclude(accepted=False).filter( Q(from_user=self.user, to_user=request.user) | Q(from_user=request.user, to_user=self.user)).count()) elif self.visibility == VISIBILITY_CUSTOM: return self.participation_set.filter( (Q(accepted=True) | Q(responded=False)), user=request.user).count() return False @staticmethod @allow_staff_or_superuser def has_write_permission(request): return request.user.type == USER_TYPE_PROJECT_OWNER @staticmethod @allow_staff_or_superuser def has_update_permission(request): return True @allow_staff_or_superuser def has_object_write_permission(self, request): return request.user == self.user @allow_staff_or_superuser def has_object_update_permission(self, request): if self.has_object_write_permission(request): return True # Participants can edit participation info directly on task object if request.method in ['PUT', 'PATCH']: allowed_keys = [ 'assignee', 'participants', 'confirmed_participants', 'rejected_participants' ] if not [x for x in request.data.keys() if not x in allowed_keys]: return self.participation_set.filter( (Q(accepted=True) | Q(responded=False)), user=request.user).count() return False def display_fee(self, amount=None): if amount is None: amount = self.fee if self.currency in CURRENCY_SYMBOLS: return '%s%s' % (CURRENCY_SYMBOLS[self.currency], floatformat(amount, arg=-2)) return amount @property def summary(self): return '%s - Fee: %s' % (self.title, self.display_fee()) @property def excerpt(self): try: return strip_tags(self.description).strip() except: return None @property def skills_list(self): return str(self.skills) @property def milestones(self): return self.progressevent_set.filter(type__in=[ PROGRESS_EVENT_TYPE_MILESTONE, PROGRESS_EVENT_TYPE_SUBMIT ]) @property def progress_events(self): return self.progressevent_set.all() @property def participation(self): return self.participation_set.filter( Q(accepted=True) | Q(responded=False)) @property def assignee(self): try: return self.participation_set.get( (Q(accepted=True) | Q(responded=False)), assignee=True) except: return None @property def update_schedule_display(self): if self.update_interval and self.update_interval_units: if self.update_interval == 1 and self.update_interval_units == UPDATE_SCHEDULE_DAILY: return 'Daily' interval_units = str( self.get_update_interval_units_display()).lower() if self.update_interval == 1: return 'Every %s' % interval_units return 'Every %s %ss' % (self.update_interval, interval_units) return None @property def applications(self): return self.application_set.filter(responded=False) @property def all_uploads(self): return Upload.objects.filter( Q(tasks=self) | Q(comments__tasks=self) | Q(progress_reports__event__task=self)) @property def meta_payment(self): return { 'task_url': '/task/%s/' % self.id, 'amount': self.fee, 'currency': self.currency } def get_default_participation(self): tags = ['tunga.io', 'tunga'] if self.skills: tags.extend(str(self.skills).split(',')) tunga_share = '{share}%'.format(**{'share': TUNGA_SHARE_PERCENTAGE}) return { 'type': 'payment', 'language': 'EN', 'title': self.summary, 'description': self.excerpt or self.summary, 'keywords': tags, 'participants': [{ 'id': 'mailto:%s' % TUNGA_SHARE_EMAIL, 'role': 'owner', 'share': tunga_share }] } def mobbr_participation(self, check_only=False): participation_meta = self.get_default_participation() if not self.url: return participation_meta, False mobbr_info_url = '%s?url=%s' % ( 'https://api.mobbr.com/api_v1/uris/info', urllib.quote_plus(self.url)) r = requests.get(mobbr_info_url, **{'headers': { 'Accept': 'application/json' }}) has_script = False if r.status_code == 200: response = r.json() task_script = response['result']['script'] for meta_key in participation_meta: if meta_key == 'keywords': if isinstance(task_script[meta_key], list): participation_meta[meta_key].extend( task_script[meta_key]) elif meta_key == 'participants': if isinstance(task_script[meta_key], list): absolute_shares = [] relative_shares = [] absolute_participants = [] relative_participants = [] for key, participant in enumerate( task_script[meta_key]): if re.match(r'\d+%$', participant['share']): share = int(participant['share'].replace( "%", "")) if share > 0: absolute_shares.append(share) new_participant = participant new_participant['share'] = share absolute_participants.append( new_participant) else: share = int(participant['share']) if share > 0: relative_shares.append(share) new_participant = participant new_participant['share'] = share relative_participants.append( new_participant) additional_participants = [] total_absolutes = sum(absolute_shares) total_relatives = sum(relative_shares) if total_absolutes >= 100 or total_relatives == 0: additional_participants = absolute_participants elif total_absolutes == 0: additional_participants = relative_participants else: additional_participants = absolute_participants for participant in relative_participants: share = int( round(((participant['share'] * (100 - total_absolutes)) / total_relatives), 0)) if share > 0: new_participant = participant new_participant['share'] = share additional_participants.append( new_participant) if len(additional_participants): participation_meta[meta_key].extend( additional_participants) has_script = True elif meta_key in task_script: participation_meta[meta_key] = task_script[meta_key] return participation_meta, has_script @property def meta_participation(self): participation_meta, has_script = self.mobbr_participation() # TODO: Update local participation script to use defined shares if not has_script: participants = self.participation_set.filter( accepted=True).order_by('share') total_shares = 100 - TUNGA_SHARE_PERCENTAGE num_participants = participants.count() for participant in participants: participation_meta['participants'].append({ 'id': 'mailto:%s' % participant.user.email, 'role': participant.role, 'share': int(total_shares / num_participants) }) return participation_meta
class Integration(models.Model): task = models.ForeignKey(Task, on_delete=models.CASCADE) provider = models.CharField(max_length=30, choices=providers.registry.as_choices()) type = models.PositiveSmallIntegerField( choices=INTEGRATION_TYPE_CHOICES, help_text=','.join([ '%s - %s' % (item[0], item[1]) for item in INTEGRATION_TYPE_CHOICES ])) events = models.ManyToManyField(IntegrationEvent, related_name='integrations') secret = models.CharField(max_length=30, default=get_random_string) created_by = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='integrations_created', blank=True, null=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __unicode__(self): return '%s - %s' % (self.get_provider_display(), self.task.summary) class Meta: unique_together = ('task', 'provider') ordering = ['created_at'] @staticmethod @allow_staff_or_superuser def has_read_permission(request): return True @allow_staff_or_superuser def has_object_read_permission(self, request): return request.user == self.task.user @staticmethod @allow_staff_or_superuser def has_write_permission(request): return request.user.type == USER_TYPE_PROJECT_OWNER @allow_staff_or_superuser def has_object_write_permission(self, request): return request.user == self.task.user @property def hook_id(self): try: return self.integrationmeta_set.get(meta_key='hook_id').meta_value except: return None @property def repo_id(self): try: return self.integrationmeta_set.get(meta_key='repo_id').meta_value except: return None @property def repo_full_name(self): try: return self.integrationmeta_set.get( meta_key='repo_full_name').meta_value except: return None @property def issue_id(self): try: return self.integrationmeta_set.get(meta_key='issue_id').meta_value except: return None @property def issue_number(self): try: return self.integrationmeta_set.get( meta_key='issue_number').meta_value except: return None