from django.db import models from django.utils.text import slugify from django.utils import timezone from django.conf import settings from django.shortcuts import resolve_url from byostorage.user import BYOStorage import random from urllib.parse import urlparse import os.path from .utils import sign_data MEDIA_TYPES = [ ('audio', "Audio"), ('video', "Video"), ('general', "General"), ] def rough_date(d): if not d: return False, "sometime..." days = (d - timezone.now()).days in_past = days < 0 if in_past: days = abs(days) if days == 0: m = int((d-timezone.now()).seconds/60) if m > 60: return in_past, "{0:d} hours".format(int(m / 60)) return in_past, "{0:d} minutes!".format(int(m % 60)) if days >= 14: return in_past, "{0:d} weeks".format(int(days/7)) if days >= 7: return in_past, "{0:d} weeks, {1:d} days".format(int(days / 7), int(days % 7)) return in_past, f"{days} days" def generate_code(length=9): return "".join([ random.choice('0123456789') for _ in range(length) ]) class EnsembleQuerySet(models.QuerySet): def for_user(self, user, ensemble_keys=[], project_keys=[]): if user.is_superuser: return self f = models.Q(slug__in=ensemble_keys) | models.Q(projects__in=project_keys) if user.is_authenticated: f |= models.Q(admins=user.pk) return self.filter(f).distinct() class Ensemble(models.Model): ''' A group that plays together ''' name = models.CharField(max_length=100, help_text="Display name") slug = models.SlugField(max_length=100, editable=False, unique=True, help_text="Short name for the ensemble - used for folders") admins = models.ManyToManyField('auth.User', related_name='ensembles') details = models.TextField(blank=True, help_text="Description of the ensemble (markdown)") storage = models.ForeignKey('byostorage.UserStorage', null=True, on_delete=models.SET_NULL, help_text="Default storage for this ensemble") nonce = models.SmallIntegerField(default=1, help_text="Increment this to reset the authentication links") objects = EnsembleQuerySet.as_manager() class Meta: ordering = ('slug', ) def active_projects(self): return self.projects.active().current() def has_admin(self, user): if not user.is_authenticated: return False if user.is_superuser: return True return user.pk in self.admins.values_list('pk', flat=True) def save(self, **kwargs): if not self.slug: self.slug = slugify(self.name) super(Ensemble, self).save(**kwargs) def get_absolute_url(self): return resolve_url('ensemble_detail', ensemble=self.slug) def auth(self): return sign_data(f'{self.pk}-{self.nonce}', 12) def __str__(self): return self.name class ProjectQuerySet(models.QuerySet): def current(self): return self.filter(models.Q(event_date__gte=(timezone.now()-timezone.timedelta(7))) | models.Q(event_date=None)) def active(self): return self.filter(active=True) def for_user(self, user, project_keys=[], ensemble_keys=[]): if user.is_superuser: return self f = models.Q(pk__in=project_keys) | models.Q(ensemble__slug__in=ensemble_keys) if user.is_authenticated: f |= models.Q(ensemble__admins=user.pk) return self.filter(f) class Project(models.Model): ''' A Project linked to an ensemble ''' name = models.CharField(max_length=100) ensemble = models.ForeignKey(Ensemble, related_name='projects', on_delete=models.CASCADE, null=True) description = models.TextField(blank=True, help_text="Markdown format") active = models.BooleanField(default=True) event_date =models.DateTimeField(null=True, blank=True) owner = models.CharField(max_length=255, blank=True) nonce = models.SmallIntegerField(default=1, help_text="Increment this to reset the authentication links") objects = ProjectQuerySet.as_manager() class Meta: ordering = ['active', 'event_date'] @property def days(self): return (self.event_date - timezone.now().date()).days @property def has_happened(self): if not self.event_date: return False return self.event_date < timezone.now() @property def rough_date(self): if not self.event_date: return "No timescale" in_past, s = rough_date(self.event_date) if in_past: return f"{s} ago" return f"In {s}" @property def folder(self): project = slugify(self.name) print(f"{self.ensemble.storage_id}:{self.ensemble.slug}/{project}") return f"{self.ensemble.storage_id}:{self.ensemble.slug}/{project}" @property def active_modules(self): return self.modules.values_list('name', flat=True) def get_absolute_url(self): return resolve_url('project_detail', project=self.pk) def auth(self): return sign_data(f'{self.pk}-{self.nonce}', 12) def __str__(self): return self.name class Module(models.Model): ''' Enable modules on a oriject ''' name = models.SlugField(max_length=20, choices=[ (x, x.title()) for x in settings.POLYPHONIC_MODULES ]) project = models.ForeignKey(Project, related_name="modules", on_delete=models.CASCADE) def __str__(self): return self.name def resource_key(resource, filename): return f'{resource.project.folder}/resources/{filename}' class Resource(models.Model): ''' A viewable file resource attached to a project e.g PDF instructions, MP3 backing track ''' project = models.ForeignKey(Project, related_name='resources', on_delete=models.CASCADE) name = models.CharField(max_length=100) description = models.TextField(blank=True) file = models.FileField(storage=BYOStorage(), upload_to=resource_key) media_type = models.CharField(max_length=10, choices=MEDIA_TYPES, default='*') visible = models.BooleanField(default=True) class Meta: ordering = ['-visible', '-pk'] def accept(self): if self.media_type == 'general': return ".*" return f"{self.media_type}/*" def __str__(self): return self.name class WikiPage(models.Model): ''' An editable wiki page for the project in markdown format ''' project = models.ForeignKey(Project, related_name='wiki_pages', on_delete=models.CASCADE) title = models.CharField(max_length=255) markdown = models.TextField() def get_absolute_url(self): return resolve_url('wiki', project=self.project_id, pk=self.pk) def __str__(self): return self.title