Format code using black

This commit is contained in:
jo 2021-05-27 16:23:02 +02:00
parent efe4fa027e
commit c27f020d73
85 changed files with 3238 additions and 2243 deletions

View file

@ -2,7 +2,8 @@
from django.apps import AppConfig
from django.db.models.signals import pre_save
class LibreTimeAPIConfig(AppConfig):
name = 'libretimeapi'
verbose_name = 'LibreTime API'
default_auto_field = 'django.db.models.AutoField'
name = "libretimeapi"
verbose_name = "LibreTime API"
default_auto_field = "django.db.models.AutoField"

View file

@ -1,21 +1,23 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.models import BaseUserManager
class UserManager(BaseUserManager):
def create_user(self, username, type, email, first_name, last_name, password):
user = self.model(username=username,
type=type,
email=email,
first_name=first_name,
last_name=last_name)
user = self.model(
username=username,
type=type,
email=email,
first_name=first_name,
last_name=last_name,
)
user.set_password(password)
user.save(using=self._db)
return user
def create_superuser(self, username, email, first_name, last_name, password):
user = self.create_user(username, 'A', email, first_name, last_name, password)
user = self.create_user(username, "A", email, first_name, last_name, password)
return user
def get_by_natural_key(self, username):
return self.get(username=username)

View file

@ -15,18 +15,20 @@ class LoginAttempt(models.Model):
class Meta:
managed = False
db_table = 'cc_login_attempts'
db_table = "cc_login_attempts"
class Session(models.Model):
sessid = models.CharField(primary_key=True, max_length=32)
userid = models.ForeignKey('User', models.DO_NOTHING, db_column='userid', blank=True, null=True)
userid = models.ForeignKey(
"User", models.DO_NOTHING, db_column="userid", blank=True, null=True
)
login = models.CharField(max_length=255, blank=True, null=True)
ts = models.DateTimeField(blank=True, null=True)
class Meta:
managed = False
db_table = 'cc_sess'
db_table = "cc_sess"
USER_TYPE_CHOICES = ()
@ -35,12 +37,14 @@ for item in USER_TYPES.items():
class User(AbstractBaseUser):
username = models.CharField(db_column='login', unique=True, max_length=255)
password = models.CharField(db_column='pass', max_length=255) # Field renamed because it was a Python reserved word.
username = models.CharField(db_column="login", unique=True, max_length=255)
password = models.CharField(
db_column="pass", max_length=255
) # Field renamed because it was a Python reserved word.
type = models.CharField(max_length=1, choices=USER_TYPE_CHOICES)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
last_login = models.DateTimeField(db_column='lastlogin', blank=True, null=True)
last_login = models.DateTimeField(db_column="lastlogin", blank=True, null=True)
lastfail = models.DateTimeField(blank=True, null=True)
skype_contact = models.CharField(max_length=1024, blank=True, null=True)
jabber_contact = models.CharField(max_length=1024, blank=True, null=True)
@ -48,13 +52,13 @@ class User(AbstractBaseUser):
cell_phone = models.CharField(max_length=1024, blank=True, null=True)
login_attempts = models.IntegerField(blank=True, null=True)
USERNAME_FIELD = 'username'
EMAIL_FIELD = 'email'
REQUIRED_FIELDS = ['type', 'email', 'first_name', 'last_name']
USERNAME_FIELD = "username"
EMAIL_FIELD = "email"
REQUIRED_FIELDS = ["type", "email", "first_name", "last_name"]
objects = UserManager()
def get_full_name(self):
return '{} {}'.format(self.first_name, self.last_name)
return "{} {}".format(self.first_name, self.last_name)
def get_short_name(self):
return self.first_name
@ -66,7 +70,7 @@ class User(AbstractBaseUser):
self.password = hashlib.md5(password.encode()).hexdigest()
def is_staff(self):
print('is_staff')
print("is_staff")
return self.type == ADMIN
def check_password(self, password):
@ -82,6 +86,7 @@ class User(AbstractBaseUser):
(managed = True), then this can be replaced with
django.contrib.auth.models.PermissionMixin.
"""
def is_superuser(self):
return self.type == ADMIN
@ -125,7 +130,7 @@ class User(AbstractBaseUser):
class Meta:
managed = False
db_table = 'cc_subjs'
db_table = "cc_subjs"
class UserToken(models.Model):
@ -139,4 +144,4 @@ class UserToken(models.Model):
class Meta:
managed = False
db_table = 'cc_subjs_token'
db_table = "cc_subjs_token"

View file

@ -4,11 +4,13 @@ from django.db import models
class CeleryTask(models.Model):
task_id = models.CharField(max_length=256)
track_reference = models.ForeignKey('ThirdPartyTrackReference', models.DO_NOTHING, db_column='track_reference')
track_reference = models.ForeignKey(
"ThirdPartyTrackReference", models.DO_NOTHING, db_column="track_reference"
)
name = models.CharField(max_length=256, blank=True, null=True)
dispatch_time = models.DateTimeField(blank=True, null=True)
status = models.CharField(max_length=256)
class Meta:
managed = False
db_table = 'celery_tasks'
db_table = "celery_tasks"

View file

@ -8,5 +8,4 @@ class Country(models.Model):
class Meta:
managed = False
db_table = 'cc_country'
db_table = "cc_country"

View file

@ -6,11 +6,20 @@ class File(models.Model):
name = models.CharField(max_length=255)
mime = models.CharField(max_length=255)
ftype = models.CharField(max_length=128)
directory = models.ForeignKey('MusicDir', models.DO_NOTHING, db_column='directory', blank=True, null=True)
directory = models.ForeignKey(
"MusicDir", models.DO_NOTHING, db_column="directory", blank=True, null=True
)
filepath = models.TextField(blank=True, null=True)
import_status = models.IntegerField()
currently_accessing = models.IntegerField(db_column='currentlyaccessing')
edited_by = models.ForeignKey('User', models.DO_NOTHING, db_column='editedby', blank=True, null=True, related_name='edited_files')
currently_accessing = models.IntegerField(db_column="currentlyaccessing")
edited_by = models.ForeignKey(
"User",
models.DO_NOTHING,
db_column="editedby",
blank=True,
null=True,
related_name="edited_files",
)
mtime = models.DateTimeField(blank=True, null=True)
utime = models.DateTimeField(blank=True, null=True)
lptime = models.DateTimeField(blank=True, null=True)
@ -59,8 +68,10 @@ class File(models.Model):
contributor = models.CharField(max_length=512, blank=True, null=True)
language = models.CharField(max_length=512, blank=True, null=True)
file_exists = models.BooleanField(blank=True, null=True)
replay_gain = models.DecimalField(max_digits=8, decimal_places=2, blank=True, null=True)
owner = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
replay_gain = models.DecimalField(
max_digits=8, decimal_places=2, blank=True, null=True
)
owner = models.ForeignKey("User", models.DO_NOTHING, blank=True, null=True)
cuein = models.DurationField(blank=True, null=True)
cueout = models.DurationField(blank=True, null=True)
silan_check = models.BooleanField(blank=True, null=True)
@ -77,10 +88,10 @@ class File(models.Model):
class Meta:
managed = False
db_table = 'cc_files'
db_table = "cc_files"
permissions = [
('change_own_file', 'Change the files where they are the owner'),
('delete_own_file', 'Delete the files where they are the owner'),
("change_own_file", "Change the files where they are the owner"),
("delete_own_file", "Delete the files where they are the owner"),
]
@ -92,15 +103,16 @@ class MusicDir(models.Model):
class Meta:
managed = False
db_table = 'cc_music_dirs'
db_table = "cc_music_dirs"
class CloudFile(models.Model):
storage_backend = models.CharField(max_length=512)
resource_id = models.TextField()
filename = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True,
db_column='cc_file_id')
filename = models.ForeignKey(
File, models.DO_NOTHING, blank=True, null=True, db_column="cc_file_id"
)
class Meta:
managed = False
db_table = 'cloud_file'
db_table = "cloud_file"

View file

@ -8,7 +8,7 @@ class Playlist(models.Model):
name = models.CharField(max_length=255)
mtime = models.DateTimeField(blank=True, null=True)
utime = models.DateTimeField(blank=True, null=True)
creator = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
creator = models.ForeignKey("User", models.DO_NOTHING, blank=True, null=True)
description = models.CharField(max_length=512, blank=True, null=True)
length = models.DurationField(blank=True, null=True)
@ -17,7 +17,7 @@ class Playlist(models.Model):
class Meta:
managed = False
db_table = 'cc_playlist'
db_table = "cc_playlist"
class PlaylistContent(models.Model):
@ -39,4 +39,4 @@ class PlaylistContent(models.Model):
class Meta:
managed = False
db_table = 'cc_playlistcontents'
db_table = "cc_playlistcontents"

View file

@ -4,13 +4,13 @@ from .files import File
class ListenerCount(models.Model):
timestamp = models.ForeignKey('Timestamp', models.DO_NOTHING)
mount_name = models.ForeignKey('MountName', models.DO_NOTHING)
timestamp = models.ForeignKey("Timestamp", models.DO_NOTHING)
mount_name = models.ForeignKey("MountName", models.DO_NOTHING)
listener_count = models.IntegerField()
class Meta:
managed = False
db_table = 'cc_listener_count'
db_table = "cc_listener_count"
class LiveLog(models.Model):
@ -20,18 +20,20 @@ class LiveLog(models.Model):
class Meta:
managed = False
db_table = 'cc_live_log'
db_table = "cc_live_log"
class PlayoutHistory(models.Model):
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
starts = models.DateTimeField()
ends = models.DateTimeField(blank=True, null=True)
instance = models.ForeignKey('ShowInstance', models.DO_NOTHING, blank=True, null=True)
instance = models.ForeignKey(
"ShowInstance", models.DO_NOTHING, blank=True, null=True
)
class Meta:
managed = False
db_table = 'cc_playout_history'
db_table = "cc_playout_history"
class PlayoutHistoryMetadata(models.Model):
@ -41,7 +43,7 @@ class PlayoutHistoryMetadata(models.Model):
class Meta:
managed = False
db_table = 'cc_playout_history_metadata'
db_table = "cc_playout_history_metadata"
class PlayoutHistoryTemplate(models.Model):
@ -50,7 +52,7 @@ class PlayoutHistoryTemplate(models.Model):
class Meta:
managed = False
db_table = 'cc_playout_history_template'
db_table = "cc_playout_history_template"
class PlayoutHistoryTemplateField(models.Model):
@ -63,7 +65,7 @@ class PlayoutHistoryTemplateField(models.Model):
class Meta:
managed = False
db_table = 'cc_playout_history_template_field'
db_table = "cc_playout_history_template_field"
class Timestamp(models.Model):
@ -71,4 +73,4 @@ class Timestamp(models.Model):
class Meta:
managed = False
db_table = 'cc_timestamp'
db_table = "cc_timestamp"

View file

@ -8,14 +8,14 @@ class ImportedPodcast(models.Model):
auto_ingest = models.BooleanField()
auto_ingest_timestamp = models.DateTimeField(blank=True, null=True)
album_override = models.BooleanField()
podcast = models.ForeignKey('Podcast', models.DO_NOTHING)
podcast = models.ForeignKey("Podcast", models.DO_NOTHING)
def get_owner(self):
return self.podcast.owner
class Meta:
managed = False
db_table = 'imported_podcast'
db_table = "imported_podcast"
class Podcast(models.Model):
@ -32,17 +32,19 @@ class Podcast(models.Model):
itunes_subtitle = models.CharField(max_length=4096, blank=True, null=True)
itunes_category = models.CharField(max_length=4096, blank=True, null=True)
itunes_explicit = models.CharField(max_length=4096, blank=True, null=True)
owner = models.ForeignKey(User, models.DO_NOTHING, db_column='owner', blank=True, null=True)
owner = models.ForeignKey(
User, models.DO_NOTHING, db_column="owner", blank=True, null=True
)
def get_owner(self):
return self.owner
class Meta:
managed = False
db_table = 'podcast'
db_table = "podcast"
permissions = [
('change_own_podcast', 'Change the podcasts where they are the owner'),
('delete_own_podcast', 'Delete the podcasts where they are the owner'),
("change_own_podcast", "Change the podcasts where they are the owner"),
("delete_own_podcast", "Delete the podcasts where they are the owner"),
]
@ -60,10 +62,16 @@ class PodcastEpisode(models.Model):
class Meta:
managed = False
db_table = 'podcast_episodes'
db_table = "podcast_episodes"
permissions = [
('change_own_podcastepisode', 'Change the episodes of podcasts where they are the owner'),
('delete_own_podcastepisode', 'Delete the episodes of podcasts where they are the owner'),
(
"change_own_podcastepisode",
"Change the episodes of podcasts where they are the owner",
),
(
"delete_own_podcastepisode",
"Delete the episodes of podcasts where they are the owner",
),
]
@ -75,4 +83,4 @@ class StationPodcast(models.Model):
class Meta:
managed = False
db_table = 'station_podcast'
db_table = "station_podcast"

View file

@ -3,14 +3,16 @@ from django.db import models
class Preference(models.Model):
subjid = models.ForeignKey('User', models.DO_NOTHING, db_column='subjid', blank=True, null=True)
subjid = models.ForeignKey(
"User", models.DO_NOTHING, db_column="subjid", blank=True, null=True
)
keystr = models.CharField(unique=True, max_length=255, blank=True, null=True)
valstr = models.TextField(blank=True, null=True)
class Meta:
managed = False
db_table = 'cc_pref'
unique_together = (('subjid', 'keystr'),)
db_table = "cc_pref"
unique_together = (("subjid", "keystr"),)
class MountName(models.Model):
@ -18,7 +20,7 @@ class MountName(models.Model):
class Meta:
managed = False
db_table = 'cc_mount_name'
db_table = "cc_mount_name"
class StreamSetting(models.Model):
@ -28,4 +30,4 @@ class StreamSetting(models.Model):
class Meta:
managed = False
db_table = 'cc_stream_setting'
db_table = "cc_stream_setting"

View file

@ -7,14 +7,14 @@ class Schedule(models.Model):
starts = models.DateTimeField()
ends = models.DateTimeField()
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
stream = models.ForeignKey('Webstream', models.DO_NOTHING, blank=True, null=True)
stream = models.ForeignKey("Webstream", models.DO_NOTHING, blank=True, null=True)
clip_length = models.DurationField(blank=True, null=True)
fade_in = models.TimeField(blank=True, null=True)
fade_out = models.TimeField(blank=True, null=True)
cue_in = models.DurationField()
cue_out = models.DurationField()
media_item_played = models.BooleanField(blank=True, null=True)
instance = models.ForeignKey('ShowInstance', models.DO_NOTHING)
instance = models.ForeignKey("ShowInstance", models.DO_NOTHING)
playout_status = models.SmallIntegerField()
broadcasted = models.SmallIntegerField()
position = models.IntegerField()
@ -24,8 +24,8 @@ class Schedule(models.Model):
class Meta:
managed = False
db_table = 'cc_schedule'
db_table = "cc_schedule"
permissions = [
('change_own_schedule', 'Change the content on their shows'),
('delete_own_schedule', 'Delete the content on their shows'),
("change_own_schedule", "Change the content on their shows"),
("delete_own_schedule", "Delete the content on their shows"),
]

View file

@ -8,5 +8,4 @@ class ServiceRegister(models.Model):
class Meta:
managed = False
db_table = 'cc_service_register'
db_table = "cc_service_register"

View file

@ -27,7 +27,7 @@ class Show(models.Model):
class Meta:
managed = False
db_table = 'cc_show'
db_table = "cc_show"
class ShowDays(models.Model):
@ -47,16 +47,16 @@ class ShowDays(models.Model):
class Meta:
managed = False
db_table = 'cc_show_days'
db_table = "cc_show_days"
class ShowHost(models.Model):
show = models.ForeignKey(Show, models.DO_NOTHING)
subjs = models.ForeignKey('User', models.DO_NOTHING)
subjs = models.ForeignKey("User", models.DO_NOTHING)
class Meta:
managed = False
db_table = 'cc_show_hosts'
db_table = "cc_show_hosts"
class ShowInstance(models.Model):
@ -66,7 +66,7 @@ class ShowInstance(models.Model):
show = models.ForeignKey(Show, models.DO_NOTHING)
record = models.SmallIntegerField(blank=True, null=True)
rebroadcast = models.SmallIntegerField(blank=True, null=True)
instance = models.ForeignKey('self', models.DO_NOTHING, blank=True, null=True)
instance = models.ForeignKey("self", models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey(File, models.DO_NOTHING, blank=True, null=True)
time_filled = models.DurationField(blank=True, null=True)
created = models.DateTimeField()
@ -79,7 +79,7 @@ class ShowInstance(models.Model):
class Meta:
managed = False
db_table = 'cc_show_instances'
db_table = "cc_show_instances"
class ShowRebroadcast(models.Model):
@ -92,4 +92,4 @@ class ShowRebroadcast(models.Model):
class Meta:
managed = False
db_table = 'cc_show_rebroadcast'
db_table = "cc_show_rebroadcast"

View file

@ -6,7 +6,7 @@ class SmartBlock(models.Model):
name = models.CharField(max_length=255)
mtime = models.DateTimeField(blank=True, null=True)
utime = models.DateTimeField(blank=True, null=True)
creator = models.ForeignKey('User', models.DO_NOTHING, blank=True, null=True)
creator = models.ForeignKey("User", models.DO_NOTHING, blank=True, null=True)
description = models.CharField(max_length=512, blank=True, null=True)
length = models.DurationField(blank=True, null=True)
type = models.CharField(max_length=7, blank=True, null=True)
@ -16,16 +16,22 @@ class SmartBlock(models.Model):
class Meta:
managed = False
db_table = 'cc_block'
db_table = "cc_block"
permissions = [
('change_own_smartblock', 'Change the smartblocks where they are the owner'),
('delete_own_smartblock', 'Delete the smartblocks where they are the owner'),
(
"change_own_smartblock",
"Change the smartblocks where they are the owner",
),
(
"delete_own_smartblock",
"Delete the smartblocks where they are the owner",
),
]
class SmartBlockContent(models.Model):
block = models.ForeignKey(SmartBlock, models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey('File', models.DO_NOTHING, blank=True, null=True)
file = models.ForeignKey("File", models.DO_NOTHING, blank=True, null=True)
position = models.IntegerField(blank=True, null=True)
trackoffset = models.FloatField()
cliplength = models.DurationField(blank=True, null=True)
@ -39,10 +45,16 @@ class SmartBlockContent(models.Model):
class Meta:
managed = False
db_table = 'cc_blockcontents'
db_table = "cc_blockcontents"
permissions = [
('change_own_smartblockcontent', 'Change the content of smartblocks where they are the owner'),
('delete_own_smartblockcontent', 'Delete the content of smartblocks where they are the owner'),
(
"change_own_smartblockcontent",
"Change the content of smartblocks where they are the owner",
),
(
"delete_own_smartblockcontent",
"Delete the content of smartblocks where they are the owner",
),
]
@ -59,9 +71,14 @@ class SmartBlockCriteria(models.Model):
class Meta:
managed = False
db_table = 'cc_blockcriteria'
db_table = "cc_blockcriteria"
permissions = [
('change_own_smartblockcriteria', 'Change the criteria of smartblocks where they are the owner'),
('delete_own_smartblockcriteria', 'Delete the criteria of smartblocks where they are the owner'),
(
"change_own_smartblockcriteria",
"Change the criteria of smartblocks where they are the owner",
),
(
"delete_own_smartblockcriteria",
"Delete the criteria of smartblocks where they are the owner",
),
]

View file

@ -12,7 +12,8 @@ class ThirdPartyTrackReference(models.Model):
class Meta:
managed = False
db_table = 'third_party_track_references'
db_table = "third_party_track_references"
class TrackType(models.Model):
code = models.CharField(max_length=16, unique=True)
@ -22,5 +23,4 @@ class TrackType(models.Model):
class Meta:
managed = False
db_table = 'cc_track_types'
db_table = "cc_track_types"

View file

@ -1,12 +1,12 @@
# -*- coding: utf-8 -*-
GUEST = 'G'
DJ = 'H'
PROGRAM_MANAGER = 'P'
ADMIN = 'A'
GUEST = "G"
DJ = "H"
PROGRAM_MANAGER = "P"
ADMIN = "A"
USER_TYPES = {
GUEST: 'Guest',
DJ: 'DJ',
PROGRAM_MANAGER: 'Program Manager',
ADMIN: 'Admin',
GUEST: "Guest",
DJ: "DJ",
PROGRAM_MANAGER: "Program Manager",
ADMIN: "Admin",
}

View file

@ -21,10 +21,10 @@ class Webstream(models.Model):
class Meta:
managed = False
db_table = 'cc_webstream'
db_table = "cc_webstream"
permissions = [
('change_own_webstream', 'Change the webstreams where they are the owner'),
('delete_own_webstream', 'Delete the webstreams where they are the owner'),
("change_own_webstream", "Change the webstreams where they are the owner"),
("delete_own_webstream", "Delete the webstreams where they are the owner"),
]
@ -38,4 +38,4 @@ class WebstreamMetadata(models.Model):
class Meta:
managed = False
db_table = 'cc_webstream_metadata'
db_table = "cc_webstream_metadata"

View file

@ -5,98 +5,101 @@ from .models.user_constants import GUEST, DJ, PROGRAM_MANAGER, USER_TYPES
logger = logging.getLogger(__name__)
GUEST_PERMISSIONS = ['view_schedule',
'view_show',
'view_showdays',
'view_showhost',
'view_showinstance',
'view_showrebroadcast',
'view_file',
'view_podcast',
'view_podcastepisode',
'view_playlist',
'view_playlistcontent',
'view_smartblock',
'view_smartblockcontent',
'view_smartblockcriteria',
'view_webstream',
'view_apiroot',
]
DJ_PERMISSIONS = GUEST_PERMISSIONS + ['add_file',
'add_podcast',
'add_podcastepisode',
'add_playlist',
'add_playlistcontent',
'add_smartblock',
'add_smartblockcontent',
'add_smartblockcriteria',
'add_webstream',
'change_own_schedule',
'change_own_file',
'change_own_podcast',
'change_own_podcastepisode',
'change_own_playlist',
'change_own_playlistcontent',
'change_own_smartblock',
'change_own_smartblockcontent',
'change_own_smartblockcriteria',
'change_own_webstream',
'delete_own_schedule',
'delete_own_file',
'delete_own_podcast',
'delete_own_podcastepisode',
'delete_own_playlist',
'delete_own_playlistcontent',
'delete_own_smartblock',
'delete_own_smartblockcontent',
'delete_own_smartblockcriteria',
'delete_own_webstream',
]
PROGRAM_MANAGER_PERMISSIONS = GUEST_PERMISSIONS + ['add_show',
'add_showdays',
'add_showhost',
'add_showinstance',
'add_showrebroadcast',
'add_file',
'add_podcast',
'add_podcastepisode',
'add_playlist',
'add_playlistcontent',
'add_smartblock',
'add_smartblockcontent',
'add_smartblockcriteria',
'add_webstream',
'change_schedule',
'change_show',
'change_showdays',
'change_showhost',
'change_showinstance',
'change_showrebroadcast',
'change_file',
'change_podcast',
'change_podcastepisode',
'change_playlist',
'change_playlistcontent',
'change_smartblock',
'change_smartblockcontent',
'change_smartblockcriteria',
'change_webstream',
'delete_schedule',
'delete_show',
'delete_showdays',
'delete_showhost',
'delete_showinstance',
'delete_showrebroadcast',
'delete_file',
'delete_podcast',
'delete_podcastepisode',
'delete_playlist',
'delete_playlistcontent',
'delete_smartblock',
'delete_smartblockcontent',
'delete_smartblockcriteria',
'delete_webstream',
]
GUEST_PERMISSIONS = [
"view_schedule",
"view_show",
"view_showdays",
"view_showhost",
"view_showinstance",
"view_showrebroadcast",
"view_file",
"view_podcast",
"view_podcastepisode",
"view_playlist",
"view_playlistcontent",
"view_smartblock",
"view_smartblockcontent",
"view_smartblockcriteria",
"view_webstream",
"view_apiroot",
]
DJ_PERMISSIONS = GUEST_PERMISSIONS + [
"add_file",
"add_podcast",
"add_podcastepisode",
"add_playlist",
"add_playlistcontent",
"add_smartblock",
"add_smartblockcontent",
"add_smartblockcriteria",
"add_webstream",
"change_own_schedule",
"change_own_file",
"change_own_podcast",
"change_own_podcastepisode",
"change_own_playlist",
"change_own_playlistcontent",
"change_own_smartblock",
"change_own_smartblockcontent",
"change_own_smartblockcriteria",
"change_own_webstream",
"delete_own_schedule",
"delete_own_file",
"delete_own_podcast",
"delete_own_podcastepisode",
"delete_own_playlist",
"delete_own_playlistcontent",
"delete_own_smartblock",
"delete_own_smartblockcontent",
"delete_own_smartblockcriteria",
"delete_own_webstream",
]
PROGRAM_MANAGER_PERMISSIONS = GUEST_PERMISSIONS + [
"add_show",
"add_showdays",
"add_showhost",
"add_showinstance",
"add_showrebroadcast",
"add_file",
"add_podcast",
"add_podcastepisode",
"add_playlist",
"add_playlistcontent",
"add_smartblock",
"add_smartblockcontent",
"add_smartblockcriteria",
"add_webstream",
"change_schedule",
"change_show",
"change_showdays",
"change_showhost",
"change_showinstance",
"change_showrebroadcast",
"change_file",
"change_podcast",
"change_podcastepisode",
"change_playlist",
"change_playlistcontent",
"change_smartblock",
"change_smartblockcontent",
"change_smartblockcriteria",
"change_webstream",
"delete_schedule",
"delete_show",
"delete_showdays",
"delete_showhost",
"delete_showinstance",
"delete_showrebroadcast",
"delete_file",
"delete_podcast",
"delete_podcastepisode",
"delete_playlist",
"delete_playlistcontent",
"delete_smartblock",
"delete_smartblockcontent",
"delete_smartblockcriteria",
"delete_webstream",
]
GROUPS = {
GUEST: GUEST_PERMISSIONS,

View file

@ -4,21 +4,22 @@ from django.conf import settings
from .models.user_constants import DJ
REQUEST_PERMISSION_TYPE_MAP = {
'GET': 'view',
'HEAD': 'view',
'OPTIONS': 'view',
'POST': 'change',
'PUT': 'change',
'DELETE': 'delete',
'PATCH': 'change',
"GET": "view",
"HEAD": "view",
"OPTIONS": "view",
"POST": "change",
"PUT": "change",
"DELETE": "delete",
"PATCH": "change",
}
def get_own_obj(request, view):
user = request.user
if user is None or user.type != DJ:
return ''
if request.method == 'GET':
return ''
return ""
if request.method == "GET":
return ""
qs = view.queryset.all()
try:
model_owners = []
@ -27,32 +28,34 @@ def get_own_obj(request, view):
if owner not in model_owners:
model_owners.append(owner)
if len(model_owners) == 1 and user in model_owners:
return 'own_'
return "own_"
except AttributeError:
return ''
return ''
return ""
return ""
def get_permission_for_view(request, view):
try:
permission_type = REQUEST_PERMISSION_TYPE_MAP[request.method]
if view.__class__.__name__ == 'APIRootView':
return '{}_apiroot'.format(permission_type)
if view.__class__.__name__ == "APIRootView":
return "{}_apiroot".format(permission_type)
model = view.model_permission_name
own_obj = get_own_obj(request, view)
return '{permission_type}_{own_obj}{model}'.format(permission_type=permission_type,
own_obj=own_obj,
model=model)
return "{permission_type}_{own_obj}{model}".format(
permission_type=permission_type, own_obj=own_obj, model=model
)
except AttributeError:
return None
def check_authorization_header(request):
auth_header = request.META.get('Authorization')
if not auth_header:
auth_header = request.META.get('HTTP_AUTHORIZATION', '')
if auth_header.startswith('Api-Key'):
def check_authorization_header(request):
auth_header = request.META.get("Authorization")
if not auth_header:
auth_header = request.META.get("HTTP_AUTHORIZATION", "")
if auth_header.startswith("Api-Key"):
token = auth_header.split()[1]
if token == settings.CONFIG.get('general', 'api_key'):
if token == settings.CONFIG.get("general", "api_key"):
return True
return False
@ -63,6 +66,7 @@ class IsAdminOrOwnUser(BasePermission):
Django's standard permission system. For details see
https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
"""
def has_permission(self, request, view):
if request.user.is_superuser():
return True
@ -84,6 +88,7 @@ class IsSystemTokenOrUser(BasePermission):
an API-Key header. All standard-users (i.e. not using the API-Key) have their
permissions checked against Django's standard permission system.
"""
def has_permission(self, request, view):
if request.user and request.user.is_authenticated:
perm = get_permission_for_view(request, view)
@ -91,7 +96,7 @@ class IsSystemTokenOrUser(BasePermission):
# model. This use-case allows users to view the base of the API
# explorer. Their assigned group permissions determine further access
# into the explorer.
if perm == 'view_apiroot':
if perm == "view_apiroot":
return True
return request.user.has_perm(perm)
return check_authorization_header(request)

View file

@ -3,264 +3,305 @@ from django.contrib.auth import get_user_model
from rest_framework import serializers
from .models import *
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = get_user_model()
fields = [
'item_url',
'username',
'type',
'first_name',
'last_name',
'lastfail',
'skype_contact',
'jabber_contact',
'email',
'cell_phone',
'login_attempts',
"item_url",
"username",
"type",
"first_name",
"last_name",
"lastfail",
"skype_contact",
"jabber_contact",
"email",
"cell_phone",
"login_attempts",
]
class SmartBlockSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlock
fields = '__all__'
fields = "__all__"
class SmartBlockContentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlockContent
fields = '__all__'
fields = "__all__"
class SmartBlockCriteriaSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = SmartBlockCriteria
fields = '__all__'
fields = "__all__"
class CountrySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Country
fields = '__all__'
fields = "__all__"
class FileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = File
fields = '__all__'
fields = "__all__"
class ListenerCountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ListenerCount
fields = '__all__'
fields = "__all__"
class LiveLogSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = LiveLog
fields = '__all__'
fields = "__all__"
class LoginAttemptSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = LoginAttempt
fields = '__all__'
fields = "__all__"
class MountNameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = MountName
fields = '__all__'
fields = "__all__"
class MusicDirSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = MusicDir
fields = '__all__'
fields = "__all__"
class PlaylistSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Playlist
fields = '__all__'
fields = "__all__"
class PlaylistContentSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlaylistContent
fields = '__all__'
fields = "__all__"
class PlayoutHistorySerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlayoutHistory
fields = '__all__'
fields = "__all__"
class PlayoutHistoryMetadataSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlayoutHistoryMetadata
fields = '__all__'
fields = "__all__"
class PlayoutHistoryTemplateSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlayoutHistoryTemplate
fields = '__all__'
fields = "__all__"
class PlayoutHistoryTemplateFieldSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PlayoutHistoryTemplateField
fields = '__all__'
fields = "__all__"
class PreferenceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Preference
fields = '__all__'
fields = "__all__"
class ScheduleSerializer(serializers.HyperlinkedModelSerializer):
file_id = serializers.IntegerField(source='file.id', read_only=True)
stream_id = serializers.IntegerField(source='stream.id', read_only=True)
instance_id = serializers.IntegerField(source='instance.id', read_only=True)
file_id = serializers.IntegerField(source="file.id", read_only=True)
stream_id = serializers.IntegerField(source="stream.id", read_only=True)
instance_id = serializers.IntegerField(source="instance.id", read_only=True)
class Meta:
model = Schedule
fields = [
'item_url',
'id',
'starts',
'ends',
'clip_length',
'fade_in',
'fade_out',
'cue_in',
'cue_out',
'media_item_played',
'file',
'file_id',
'stream',
'stream_id',
'instance',
'instance_id',
"item_url",
"id",
"starts",
"ends",
"clip_length",
"fade_in",
"fade_out",
"cue_in",
"cue_out",
"media_item_played",
"file",
"file_id",
"stream",
"stream_id",
"instance",
"instance_id",
]
class ServiceRegisterSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ServiceRegister
fields = '__all__'
fields = "__all__"
class SessionSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Session
fields = '__all__'
fields = "__all__"
class ShowSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Show
fields = [
'item_url',
'id',
'name',
'url',
'genre',
'description',
'color',
'background_color',
'linked',
'is_linkable',
'image_path',
'has_autoplaylist',
'autoplaylist_repeat',
'autoplaylist',
"item_url",
"id",
"name",
"url",
"genre",
"description",
"color",
"background_color",
"linked",
"is_linkable",
"image_path",
"has_autoplaylist",
"autoplaylist_repeat",
"autoplaylist",
]
class ShowDaysSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowDays
fields = '__all__'
fields = "__all__"
class ShowHostSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowHost
fields = '__all__'
fields = "__all__"
class ShowInstanceSerializer(serializers.HyperlinkedModelSerializer):
show_id = serializers.IntegerField(source='show.id', read_only=True)
file_id = serializers.IntegerField(source='file.id', read_only=True)
show_id = serializers.IntegerField(source="show.id", read_only=True)
file_id = serializers.IntegerField(source="file.id", read_only=True)
class Meta:
model = ShowInstance
fields = [
'item_url',
'id',
'description',
'starts',
'ends',
'record',
'rebroadcast',
'time_filled',
'created',
'last_scheduled',
'modified_instance',
'autoplaylist_built',
'show',
'show_id',
'instance',
'file',
'file_id',
"item_url",
"id",
"description",
"starts",
"ends",
"record",
"rebroadcast",
"time_filled",
"created",
"last_scheduled",
"modified_instance",
"autoplaylist_built",
"show",
"show_id",
"instance",
"file",
"file_id",
]
class ShowRebroadcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ShowRebroadcast
fields = '__all__'
fields = "__all__"
class StreamSettingSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = StreamSetting
fields = '__all__'
fields = "__all__"
class UserTokenSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = UserToken
fields = '__all__'
fields = "__all__"
class TimestampSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Timestamp
fields = '__all__'
fields = "__all__"
class WebstreamSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Webstream
fields = '__all__'
fields = "__all__"
class WebstreamMetadataSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = WebstreamMetadata
fields = '__all__'
fields = "__all__"
class CeleryTaskSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = CeleryTask
fields = '__all__'
fields = "__all__"
class CloudFileSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = CloudFile
fields = '__all__'
fields = "__all__"
class ImportedPodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ImportedPodcast
fields = '__all__'
fields = "__all__"
class PodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Podcast
fields = '__all__'
fields = "__all__"
class PodcastEpisodeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = PodcastEpisode
fields = '__all__'
fields = "__all__"
class StationPodcastSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = StationPodcast
fields = '__all__'
fields = "__all__"
class ThirdPartyTrackReferenceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = ThirdPartyTrackReference
fields = '__all__'
fields = "__all__"
class TrackTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = TrackType
fields = '__all__'
fields = "__all__"

View file

@ -3,10 +3,11 @@ import configparser
import os
from .utils import read_config_file, get_random_string
LIBRETIME_CONF_DIR = os.getenv('LIBRETIME_CONF_DIR', '/etc/airtime')
DEFAULT_CONFIG_PATH = os.getenv('LIBRETIME_CONF_FILE',
os.path.join(LIBRETIME_CONF_DIR, 'airtime.conf'))
API_VERSION = '2.0.0'
LIBRETIME_CONF_DIR = os.getenv("LIBRETIME_CONF_DIR", "/etc/airtime")
DEFAULT_CONFIG_PATH = os.getenv(
"LIBRETIME_CONF_FILE", os.path.join(LIBRETIME_CONF_DIR, "airtime.conf")
)
API_VERSION = "2.0.0"
try:
CONFIG = read_config_file(DEFAULT_CONFIG_PATH)
@ -18,70 +19,70 @@ except IOError:
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = get_random_string(CONFIG.get('general', 'api_key', fallback=''))
SECRET_KEY = get_random_string(CONFIG.get("general", "api_key", fallback=""))
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.getenv('LIBRETIME_DEBUG', False)
DEBUG = os.getenv("LIBRETIME_DEBUG", False)
ALLOWED_HOSTS = ['*']
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
'libretimeapi.apps.LibreTimeAPIConfig',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'url_filter',
"libretimeapi.apps.LibreTimeAPIConfig",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"url_filter",
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'libretimeapi.urls'
ROOT_URLCONF = "libretimeapi.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'libretimeapi.wsgi.application'
WSGI_APPLICATION = "libretimeapi.wsgi.application"
# Database
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': CONFIG.get('database', 'dbname', fallback=''),
'USER': CONFIG.get('database', 'dbuser', fallback=''),
'PASSWORD': CONFIG.get('database', 'dbpass', fallback=''),
'HOST': CONFIG.get('database', 'host', fallback=''),
'PORT': '5432',
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": CONFIG.get("database", "dbname", fallback=""),
"USER": CONFIG.get("database", "dbuser", fallback=""),
"PASSWORD": CONFIG.get("database", "dbpass", fallback=""),
"HOST": CONFIG.get("database", "host", fallback=""),
"PORT": "5432",
}
}
@ -91,40 +92,40 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.BasicAuthentication",
),
'DEFAULT_PERMISSION_CLASSES': [
'libretimeapi.permissions.IsSystemTokenOrUser',
"DEFAULT_PERMISSION_CLASSES": [
"libretimeapi.permissions.IsSystemTokenOrUser",
],
'DEFAULT_FILTER_BACKENDS': [
'url_filter.integrations.drf.DjangoFilterBackend',
"DEFAULT_FILTER_BACKENDS": [
"url_filter.integrations.drf.DjangoFilterBackend",
],
'URL_FIELD_NAME': 'item_url',
"URL_FIELD_NAME": "item_url",
}
# Internationalization
# https://docs.djangoproject.com/en/3.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
@ -136,50 +137,53 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/api/static/'
STATIC_URL = "/api/static/"
if not DEBUG:
STATIC_ROOT = os.getenv('LIBRETIME_STATIC_ROOT', '/usr/share/airtime/api')
STATIC_ROOT = os.getenv("LIBRETIME_STATIC_ROOT", "/usr/share/airtime/api")
AUTH_USER_MODEL = 'libretimeapi.User'
AUTH_USER_MODEL = "libretimeapi.User"
TEST_RUNNER = 'libretimeapi.tests.runners.ManagedModelTestRunner'
TEST_RUNNER = "libretimeapi.tests.runners.ManagedModelTestRunner"
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'simple': {
'format': '{levelname} {message}',
'style': '{',
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"simple": {
"format": "{levelname} {message}",
"style": "{",
},
'verbose': {
'format': '{asctime} {module} {levelname} {message}',
'style': '{',
}
},
'handlers': {
'file': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': os.path.join(CONFIG.get('pypo', 'log_base_dir', fallback='.').replace('\'',''), 'api.log'),
'formatter': 'verbose',
},
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
'formatter': 'simple',
"verbose": {
"format": "{asctime} {module} {levelname} {message}",
"style": "{",
},
},
'loggers': {
'django': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propogate': True,
"handlers": {
"file": {
"level": "DEBUG",
"class": "logging.FileHandler",
"filename": os.path.join(
CONFIG.get("pypo", "log_base_dir", fallback=".").replace("'", ""),
"api.log",
),
"formatter": "verbose",
},
'libretimeapi': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propogate': True,
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
},
"loggers": {
"django": {
"handlers": ["file", "console"],
"level": "INFO",
"propogate": True,
},
"libretimeapi": {
"handlers": ["file", "console"],
"level": "INFO",
"propogate": True,
},
},
}

View file

@ -8,18 +8,17 @@ class ManagedModelTestRunner(DiscoverRunner):
project managed for the duration of the test run, so that one doesn't need
to execute the SQL manually to create them.
"""
def setup_test_environment(self, *args, **kwargs):
from django.apps import apps
self.unmanaged_models = [m for m in apps.get_models()
if not m._meta.managed]
self.unmanaged_models = [m for m in apps.get_models() if not m._meta.managed]
for m in self.unmanaged_models:
m._meta.managed = True
super(ManagedModelTestRunner, self).setup_test_environment(*args,
**kwargs)
super(ManagedModelTestRunner, self).setup_test_environment(*args, **kwargs)
def teardown_test_environment(self, *args, **kwargs):
super(ManagedModelTestRunner, self).teardown_test_environment(*args,
**kwargs)
super(ManagedModelTestRunner, self).teardown_test_environment(*args, **kwargs)
# reset unmanaged models
for m in self.unmanaged_models:
m._meta.managed = False

View file

@ -9,33 +9,40 @@ from libretimeapi.permission_constants import GROUPS
class TestUserManager(APITestCase):
def test_create_user(self):
user = User.objects.create_user('test',
email='test@example.com',
password='test',
type=DJ,
first_name='test',
last_name='user')
user = User.objects.create_user(
"test",
email="test@example.com",
password="test",
type=DJ,
first_name="test",
last_name="user",
)
db_user = User.objects.get(pk=user.pk)
self.assertEqual(db_user.username, user.username)
def test_create_superuser(self):
user = User.objects.create_superuser('test',
email='test@example.com',
password='test',
first_name='test',
last_name='user')
user = User.objects.create_superuser(
"test",
email="test@example.com",
password="test",
first_name="test",
last_name="user",
)
db_user = User.objects.get(pk=user.pk)
self.assertEqual(db_user.username, user.username)
class TestUser(APITestCase):
def test_guest_get_group_perms(self):
user = User.objects.create_user('test',
email='test@example.com',
password='test',
type=GUEST,
first_name='test',
last_name='user')
user = User.objects.create_user(
"test",
email="test@example.com",
password="test",
type=GUEST,
first_name="test",
last_name="user",
)
permissions = user.get_group_permissions()
# APIRoot permission hardcoded in the check as it isn't a Permission object
str_perms = [p.codename for p in permissions] + ['view_apiroot']
str_perms = [p.codename for p in permissions] + ["view_apiroot"]
self.assertCountEqual(str_perms, GROUPS[GUEST])

View file

@ -6,7 +6,11 @@ from django.conf import settings
from rest_framework.test import APITestCase, APIRequestFactory
from model_bakery import baker
from libretimeapi.permissions import IsSystemTokenOrUser
from libretimeapi.permission_constants import GUEST_PERMISSIONS, DJ_PERMISSIONS, PROGRAM_MANAGER_PERMISSIONS
from libretimeapi.permission_constants import (
GUEST_PERMISSIONS,
DJ_PERMISSIONS,
PROGRAM_MANAGER_PERMISSIONS,
)
from libretimeapi.models.user_constants import GUEST, DJ, PROGRAM_MANAGER, ADMIN
@ -16,54 +20,56 @@ class TestIsSystemTokenOrUser(APITestCase):
cls.path = "/api/v2/files/"
def test_unauthorized(self):
response = self.client.get(self.path.format('files'))
response = self.client.get(self.path.format("files"))
self.assertEqual(response.status_code, 403)
def test_token_incorrect(self):
token = 'doesnotexist'
token = "doesnotexist"
request = APIRequestFactory().get(self.path)
request.user = AnonymousUser()
request.META['Authorization'] = 'Api-Key {token}'.format(token=token)
request.META["Authorization"] = "Api-Key {token}".format(token=token)
allowed = IsSystemTokenOrUser().has_permission(request, None)
self.assertFalse(allowed)
def test_token_correct(self):
token = settings.CONFIG.get('general', 'api_key')
token = settings.CONFIG.get("general", "api_key")
request = APIRequestFactory().get(self.path)
request.user = AnonymousUser()
request.META['Authorization'] = 'Api-Key {token}'.format(token=token)
request.META["Authorization"] = "Api-Key {token}".format(token=token)
allowed = IsSystemTokenOrUser().has_permission(request, None)
self.assertTrue(allowed)
class TestPermissions(APITestCase):
URLS = [
'schedule',
'shows',
'show-days',
'show-hosts',
'show-instances',
'show-rebroadcasts',
'files',
'playlists',
'playlist-contents',
'smart-blocks',
'smart-block-contents',
'smart-block-criteria',
'webstreams',
"schedule",
"shows",
"show-days",
"show-hosts",
"show-instances",
"show-rebroadcasts",
"files",
"playlists",
"playlist-contents",
"smart-blocks",
"smart-block-contents",
"smart-block-criteria",
"webstreams",
]
def logged_in_test_model(self, model, name, user_type, fn):
path = self.path.format(model)
user_created = get_user_model().objects.filter(username=name)
if not user_created:
user = get_user_model().objects.create_user(name,
email='test@example.com',
password='test',
type=user_type,
first_name='test',
last_name='user')
self.client.login(username=name, password='test')
user = get_user_model().objects.create_user(
name,
email="test@example.com",
password="test",
type=user_type,
first_name="test",
last_name="user",
)
self.client.login(username=name, password="test")
return fn(path)
@classmethod
@ -72,49 +78,57 @@ class TestPermissions(APITestCase):
def test_guest_permissions_success(self):
for model in self.URLS:
response = self.logged_in_test_model(model, 'guest', GUEST, self.client.get)
self.assertEqual(response.status_code, 200,
msg='Invalid for model {}'.format(model))
response = self.logged_in_test_model(model, "guest", GUEST, self.client.get)
self.assertEqual(
response.status_code, 200, msg="Invalid for model {}".format(model)
)
def test_guest_permissions_failure(self):
for model in self.URLS:
response = self.logged_in_test_model(model, 'guest', GUEST, self.client.post)
self.assertEqual(response.status_code, 403,
msg='Invalid for model {}'.format(model))
response = self.logged_in_test_model('users', 'guest', GUEST, self.client.get)
self.assertEqual(response.status_code, 403, msg='Invalid for model users')
response = self.logged_in_test_model(
model, "guest", GUEST, self.client.post
)
self.assertEqual(
response.status_code, 403, msg="Invalid for model {}".format(model)
)
response = self.logged_in_test_model("users", "guest", GUEST, self.client.get)
self.assertEqual(response.status_code, 403, msg="Invalid for model users")
def test_dj_get_permissions(self):
for model in self.URLS:
response = self.logged_in_test_model(model, 'dj', DJ, self.client.get)
self.assertEqual(response.status_code, 200,
msg='Invalid for model {}'.format(model))
response = self.logged_in_test_model(model, "dj", DJ, self.client.get)
self.assertEqual(
response.status_code, 200, msg="Invalid for model {}".format(model)
)
def test_dj_post_permissions(self):
user = get_user_model().objects.create_user('test-dj',
email='test@example.com',
password='test',
type=DJ,
first_name='test',
last_name='user')
f = baker.make('libretimeapi.File',
owner=user)
model = 'files/{}'.format(f.id)
user = get_user_model().objects.create_user(
"test-dj",
email="test@example.com",
password="test",
type=DJ,
first_name="test",
last_name="user",
)
f = baker.make("libretimeapi.File", owner=user)
model = "files/{}".format(f.id)
path = self.path.format(model)
self.client.login(username='test-dj', password='test')
response = self.client.patch(path, {'name': 'newFilename'})
self.client.login(username="test-dj", password="test")
response = self.client.patch(path, {"name": "newFilename"})
self.assertEqual(response.status_code, 200)
def test_dj_post_permissions_failure(self):
user = get_user_model().objects.create_user('test-dj',
email='test@example.com',
password='test',
type=DJ,
first_name='test',
last_name='user')
f = baker.make('libretimeapi.File')
model = 'files/{}'.format(f.id)
user = get_user_model().objects.create_user(
"test-dj",
email="test@example.com",
password="test",
type=DJ,
first_name="test",
last_name="user",
)
f = baker.make("libretimeapi.File")
model = "files/{}".format(f.id)
path = self.path.format(model)
self.client.login(username='test-dj', password='test')
response = self.client.patch(path, {'name': 'newFilename'})
self.client.login(username="test-dj", password="test")
response = self.client.patch(path, {"name": "newFilename"})
self.assertEqual(response.status_code, 403)

View file

@ -11,29 +11,32 @@ class TestFileViewSet(APITestCase):
@classmethod
def setUpTestData(cls):
cls.path = "/api/v2/files/{id}/download/"
cls.token = settings.CONFIG.get('general', 'api_key')
cls.token = settings.CONFIG.get("general", "api_key")
def test_invalid(self):
path = self.path.format(id='a')
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
path = self.path.format(id="a")
self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
response = self.client.get(path)
self.assertEqual(response.status_code, 400)
def test_does_not_exist(self):
path = self.path.format(id='1')
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
path = self.path.format(id="1")
self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
def test_exists(self):
music_dir = baker.make('libretimeapi.MusicDir',
directory=os.path.join(os.path.dirname(__file__),
'resources'))
f = baker.make('libretimeapi.File',
directory=music_dir,
mime='audio/mp3',
filepath='song.mp3')
music_dir = baker.make(
"libretimeapi.MusicDir",
directory=os.path.join(os.path.dirname(__file__), "resources"),
)
f = baker.make(
"libretimeapi.File",
directory=music_dir,
mime="audio/mp3",
filepath="song.mp3",
)
path = self.path.format(id=str(f.pk))
self.client.credentials(HTTP_AUTHORIZATION='Api-Key {}'.format(self.token))
self.client.credentials(HTTP_AUTHORIZATION="Api-Key {}".format(self.token))
response = self.client.get(path)
self.assertEqual(response.status_code, 200)

View file

@ -5,48 +5,48 @@ from rest_framework import routers
from .views import *
router = routers.DefaultRouter()
router.register('smart-blocks', SmartBlockViewSet)
router.register('smart-block-contents', SmartBlockContentViewSet)
router.register('smart-block-criteria', SmartBlockCriteriaViewSet)
router.register('countries', CountryViewSet)
router.register('files', FileViewSet)
router.register('listener-counts', ListenerCountViewSet)
router.register('live-logs', LiveLogViewSet)
router.register('login-attempts', LoginAttemptViewSet)
router.register('mount-names', MountNameViewSet)
router.register('music-dirs', MusicDirViewSet)
router.register('playlists', PlaylistViewSet)
router.register('playlist-contents', PlaylistContentViewSet)
router.register('playout-history', PlayoutHistoryViewSet)
router.register('playout-history-metadata', PlayoutHistoryMetadataViewSet)
router.register('playout-history-templates', PlayoutHistoryTemplateViewSet)
router.register('playout-history-template-fields', PlayoutHistoryTemplateFieldViewSet)
router.register('preferences', PreferenceViewSet)
router.register('schedule', ScheduleViewSet)
router.register('service-registers', ServiceRegisterViewSet)
router.register('sessions', SessionViewSet)
router.register('shows', ShowViewSet)
router.register('show-days', ShowDaysViewSet)
router.register('show-hosts', ShowHostViewSet)
router.register('show-instances', ShowInstanceViewSet)
router.register('show-rebroadcasts', ShowRebroadcastViewSet)
router.register('stream-settings', StreamSettingViewSet)
router.register('users', UserViewSet)
router.register('user-tokens', UserTokenViewSet)
router.register('timestamps', TimestampViewSet)
router.register('webstreams', WebstreamViewSet)
router.register('webstream-metadata', WebstreamMetadataViewSet)
router.register('celery-tasks', CeleryTaskViewSet)
router.register('cloud-files', CloudFileViewSet)
router.register('imported-podcasts', ImportedPodcastViewSet)
router.register('podcasts', PodcastViewSet)
router.register('podcast-episodes', PodcastEpisodeViewSet)
router.register('station-podcasts', StationPodcastViewSet)
router.register('third-party-track-references', ThirdPartyTrackReferenceViewSet)
router.register('track-types', TrackTypeViewSet)
router.register("smart-blocks", SmartBlockViewSet)
router.register("smart-block-contents", SmartBlockContentViewSet)
router.register("smart-block-criteria", SmartBlockCriteriaViewSet)
router.register("countries", CountryViewSet)
router.register("files", FileViewSet)
router.register("listener-counts", ListenerCountViewSet)
router.register("live-logs", LiveLogViewSet)
router.register("login-attempts", LoginAttemptViewSet)
router.register("mount-names", MountNameViewSet)
router.register("music-dirs", MusicDirViewSet)
router.register("playlists", PlaylistViewSet)
router.register("playlist-contents", PlaylistContentViewSet)
router.register("playout-history", PlayoutHistoryViewSet)
router.register("playout-history-metadata", PlayoutHistoryMetadataViewSet)
router.register("playout-history-templates", PlayoutHistoryTemplateViewSet)
router.register("playout-history-template-fields", PlayoutHistoryTemplateFieldViewSet)
router.register("preferences", PreferenceViewSet)
router.register("schedule", ScheduleViewSet)
router.register("service-registers", ServiceRegisterViewSet)
router.register("sessions", SessionViewSet)
router.register("shows", ShowViewSet)
router.register("show-days", ShowDaysViewSet)
router.register("show-hosts", ShowHostViewSet)
router.register("show-instances", ShowInstanceViewSet)
router.register("show-rebroadcasts", ShowRebroadcastViewSet)
router.register("stream-settings", StreamSettingViewSet)
router.register("users", UserViewSet)
router.register("user-tokens", UserTokenViewSet)
router.register("timestamps", TimestampViewSet)
router.register("webstreams", WebstreamViewSet)
router.register("webstream-metadata", WebstreamMetadataViewSet)
router.register("celery-tasks", CeleryTaskViewSet)
router.register("cloud-files", CloudFileViewSet)
router.register("imported-podcasts", ImportedPodcastViewSet)
router.register("podcasts", PodcastViewSet)
router.register("podcast-episodes", PodcastEpisodeViewSet)
router.register("station-podcasts", StationPodcastViewSet)
router.register("third-party-track-references", ThirdPartyTrackReferenceViewSet)
router.register("track-types", TrackTypeViewSet)
urlpatterns = [
path('api/v2/', include(router.urls)),
path('api/v2/version/', version),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path("api/v2/", include(router.urls)),
path("api/v2/version/", version),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]

View file

@ -4,23 +4,27 @@ import sys
import string
import random
def read_config_file(config_path):
"""Parse the application's config file located at config_path."""
config = configparser.ConfigParser()
try:
config.readfp(open(config_path))
except IOError as e:
print("Failed to open config file at {}: {}".format(config_path, e.strerror),
file=sys.stderr)
print(
"Failed to open config file at {}: {}".format(config_path, e.strerror),
file=sys.stderr,
)
raise e
except Exception as e:
print(e.strerror, file=sys.stderr)
raise e
return config
def get_random_string(seed):
"""Generates a random string based on the given seed"""
choices = string.ascii_letters + string.digits + string.punctuation
seed = seed.encode('utf-8')
seed = seed.encode("utf-8")
rand = random.Random(seed)
return [rand.choice(choices) for i in range(16)]

View file

@ -10,220 +10,261 @@ from rest_framework.response import Response
from .serializers import *
from .permissions import IsAdminOrOwnUser
class UserViewSet(viewsets.ModelViewSet):
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
permission_classes = [IsAdminOrOwnUser]
model_permission_name = 'user'
model_permission_name = "user"
class SmartBlockViewSet(viewsets.ModelViewSet):
queryset = SmartBlock.objects.all()
serializer_class = SmartBlockSerializer
model_permission_name = 'smartblock'
model_permission_name = "smartblock"
class SmartBlockContentViewSet(viewsets.ModelViewSet):
queryset = SmartBlockContent.objects.all()
serializer_class = SmartBlockContentSerializer
model_permission_name = 'smartblockcontent'
model_permission_name = "smartblockcontent"
class SmartBlockCriteriaViewSet(viewsets.ModelViewSet):
queryset = SmartBlockCriteria.objects.all()
serializer_class = SmartBlockCriteriaSerializer
model_permission_name = 'smartblockcriteria'
model_permission_name = "smartblockcriteria"
class CountryViewSet(viewsets.ModelViewSet):
queryset = Country.objects.all()
serializer_class = CountrySerializer
model_permission_name = 'country'
model_permission_name = "country"
class FileViewSet(viewsets.ModelViewSet):
queryset = File.objects.all()
serializer_class = FileSerializer
model_permission_name = 'file'
model_permission_name = "file"
@action(detail=True, methods=['GET'])
@action(detail=True, methods=["GET"])
def download(self, request, pk=None):
if pk is None:
return Response('No file requested', status=status.HTTP_400_BAD_REQUEST)
return Response("No file requested", status=status.HTTP_400_BAD_REQUEST)
try:
pk = int(pk)
except ValueError:
return Response('File ID should be an integer',
status=status.HTTP_400_BAD_REQUEST)
return Response(
"File ID should be an integer", status=status.HTTP_400_BAD_REQUEST
)
filename = get_object_or_404(File, pk=pk)
directory = filename.directory
path = os.path.join(directory.directory, filename.filepath)
response = FileResponse(open(path, 'rb'), content_type=filename.mime)
response = FileResponse(open(path, "rb"), content_type=filename.mime)
return response
class ListenerCountViewSet(viewsets.ModelViewSet):
queryset = ListenerCount.objects.all()
serializer_class = ListenerCountSerializer
model_permission_name = 'listenercount'
model_permission_name = "listenercount"
class LiveLogViewSet(viewsets.ModelViewSet):
queryset = LiveLog.objects.all()
serializer_class = LiveLogSerializer
model_permission_name = 'livelog'
model_permission_name = "livelog"
class LoginAttemptViewSet(viewsets.ModelViewSet):
queryset = LoginAttempt.objects.all()
serializer_class = LoginAttemptSerializer
model_permission_name = 'loginattempt'
model_permission_name = "loginattempt"
class MountNameViewSet(viewsets.ModelViewSet):
queryset = MountName.objects.all()
serializer_class = MountNameSerializer
model_permission_name = 'mountname'
model_permission_name = "mountname"
class MusicDirViewSet(viewsets.ModelViewSet):
queryset = MusicDir.objects.all()
serializer_class = MusicDirSerializer
model_permission_name = 'musicdir'
model_permission_name = "musicdir"
class PlaylistViewSet(viewsets.ModelViewSet):
queryset = Playlist.objects.all()
serializer_class = PlaylistSerializer
model_permission_name = 'playlist'
model_permission_name = "playlist"
class PlaylistContentViewSet(viewsets.ModelViewSet):
queryset = PlaylistContent.objects.all()
serializer_class = PlaylistContentSerializer
model_permission_name = 'playlistcontent'
model_permission_name = "playlistcontent"
class PlayoutHistoryViewSet(viewsets.ModelViewSet):
queryset = PlayoutHistory.objects.all()
serializer_class = PlayoutHistorySerializer
model_permission_name = 'playouthistory'
model_permission_name = "playouthistory"
class PlayoutHistoryMetadataViewSet(viewsets.ModelViewSet):
queryset = PlayoutHistoryMetadata.objects.all()
serializer_class = PlayoutHistoryMetadataSerializer
model_permission_name = 'playouthistorymetadata'
model_permission_name = "playouthistorymetadata"
class PlayoutHistoryTemplateViewSet(viewsets.ModelViewSet):
queryset = PlayoutHistoryTemplate.objects.all()
serializer_class = PlayoutHistoryTemplateSerializer
model_permission_name = 'playouthistorytemplate'
model_permission_name = "playouthistorytemplate"
class PlayoutHistoryTemplateFieldViewSet(viewsets.ModelViewSet):
queryset = PlayoutHistoryTemplateField.objects.all()
serializer_class = PlayoutHistoryTemplateFieldSerializer
model_permission_name = 'playouthistorytemplatefield'
model_permission_name = "playouthistorytemplatefield"
class PreferenceViewSet(viewsets.ModelViewSet):
queryset = Preference.objects.all()
serializer_class = PreferenceSerializer
model_permission_name = 'perference'
model_permission_name = "perference"
class ScheduleViewSet(viewsets.ModelViewSet):
queryset = Schedule.objects.all()
serializer_class = ScheduleSerializer
filter_fields = ('starts', 'ends', 'playout_status', 'broadcasted')
model_permission_name = 'schedule'
filter_fields = ("starts", "ends", "playout_status", "broadcasted")
model_permission_name = "schedule"
class ServiceRegisterViewSet(viewsets.ModelViewSet):
queryset = ServiceRegister.objects.all()
serializer_class = ServiceRegisterSerializer
model_permission_name = 'serviceregister'
model_permission_name = "serviceregister"
class SessionViewSet(viewsets.ModelViewSet):
queryset = Session.objects.all()
serializer_class = SessionSerializer
model_permission_name = 'session'
model_permission_name = "session"
class ShowViewSet(viewsets.ModelViewSet):
queryset = Show.objects.all()
serializer_class = ShowSerializer
model_permission_name = 'show'
model_permission_name = "show"
class ShowDaysViewSet(viewsets.ModelViewSet):
queryset = ShowDays.objects.all()
serializer_class = ShowDaysSerializer
model_permission_name = 'showdays'
model_permission_name = "showdays"
class ShowHostViewSet(viewsets.ModelViewSet):
queryset = ShowHost.objects.all()
serializer_class = ShowHostSerializer
model_permission_name = 'showhost'
model_permission_name = "showhost"
class ShowInstanceViewSet(viewsets.ModelViewSet):
queryset = ShowInstance.objects.all()
serializer_class = ShowInstanceSerializer
model_permission_name = 'showinstance'
model_permission_name = "showinstance"
class ShowRebroadcastViewSet(viewsets.ModelViewSet):
queryset = ShowRebroadcast.objects.all()
serializer_class = ShowRebroadcastSerializer
model_permission_name = 'showrebroadcast'
model_permission_name = "showrebroadcast"
class StreamSettingViewSet(viewsets.ModelViewSet):
queryset = StreamSetting.objects.all()
serializer_class = StreamSettingSerializer
model_permission_name = 'streamsetting'
model_permission_name = "streamsetting"
class UserTokenViewSet(viewsets.ModelViewSet):
queryset = UserToken.objects.all()
serializer_class = UserTokenSerializer
model_permission_name = 'usertoken'
model_permission_name = "usertoken"
class TimestampViewSet(viewsets.ModelViewSet):
queryset = Timestamp.objects.all()
serializer_class = TimestampSerializer
model_permission_name = 'timestamp'
model_permission_name = "timestamp"
class WebstreamViewSet(viewsets.ModelViewSet):
queryset = Webstream.objects.all()
serializer_class = WebstreamSerializer
model_permission_name = 'webstream'
model_permission_name = "webstream"
class WebstreamMetadataViewSet(viewsets.ModelViewSet):
queryset = WebstreamMetadata.objects.all()
serializer_class = WebstreamMetadataSerializer
model_permission_name = 'webstreametadata'
model_permission_name = "webstreametadata"
class CeleryTaskViewSet(viewsets.ModelViewSet):
queryset = CeleryTask.objects.all()
serializer_class = CeleryTaskSerializer
model_permission_name = 'celerytask'
model_permission_name = "celerytask"
class CloudFileViewSet(viewsets.ModelViewSet):
queryset = CloudFile.objects.all()
serializer_class = CloudFileSerializer
model_permission_name = 'cloudfile'
model_permission_name = "cloudfile"
class ImportedPodcastViewSet(viewsets.ModelViewSet):
queryset = ImportedPodcast.objects.all()
serializer_class = ImportedPodcastSerializer
model_permission_name = 'importedpodcast'
model_permission_name = "importedpodcast"
class PodcastViewSet(viewsets.ModelViewSet):
queryset = Podcast.objects.all()
serializer_class = PodcastSerializer
model_permission_name = 'podcast'
model_permission_name = "podcast"
class PodcastEpisodeViewSet(viewsets.ModelViewSet):
queryset = PodcastEpisode.objects.all()
serializer_class = PodcastEpisodeSerializer
model_permission_name = 'podcastepisode'
model_permission_name = "podcastepisode"
class StationPodcastViewSet(viewsets.ModelViewSet):
queryset = StationPodcast.objects.all()
serializer_class = StationPodcastSerializer
model_permission_name = 'station'
model_permission_name = "station"
class ThirdPartyTrackReferenceViewSet(viewsets.ModelViewSet):
queryset = ThirdPartyTrackReference.objects.all()
serializer_class = ThirdPartyTrackReferenceSerializer
model_permission_name = 'thirdpartytrackreference'
model_permission_name = "thirdpartytrackreference"
class TrackTypeViewSet(viewsets.ModelViewSet):
queryset = TrackType.objects.all()
serializer_class = TrackTypeSerializer
model_permission_name = 'tracktype'
model_permission_name = "tracktype"
@api_view(['GET'])
@permission_classes((AllowAny, ))
@api_view(["GET"])
@permission_classes((AllowAny,))
def version(request, *args, **kwargs):
return Response({'api_version': settings.API_VERSION})
return Response({"api_version": settings.API_VERSION})

View file

@ -12,6 +12,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'libretimeapi.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "libretimeapi.settings")
application = get_wsgi_application()

View file

@ -8,26 +8,26 @@ print(script_path)
os.chdir(script_path)
setup(
name='libretime-api',
version='2.0.0a1',
name="libretime-api",
version="2.0.0a1",
packages=find_packages(),
include_package_data=True,
description='LibreTime API backend server',
url='https://github.com/LibreTime/libretime',
author='LibreTime Contributors',
scripts=['bin/libretime-api'],
description="LibreTime API backend server",
url="https://github.com/LibreTime/libretime",
author="LibreTime Contributors",
scripts=["bin/libretime-api"],
install_requires=[
'coreapi',
'Django~=3.0',
'djangorestframework',
'django-url-filter',
'markdown',
'model_bakery',
'psycopg2',
"coreapi",
"Django~=3.0",
"djangorestframework",
"django-url-filter",
"markdown",
"model_bakery",
"psycopg2",
],
project_urls={
'Bug Tracker': 'https://github.com/LibreTime/libretime/issues',
'Documentation': 'https://libretime.org',
'Source Code': 'https://github.com/LibreTime/libretime',
"Bug Tracker": "https://github.com/LibreTime/libretime/issues",
"Documentation": "https://libretime.org",
"Source Code": "https://github.com/LibreTime/libretime",
},
)