diff --git a/Dockerfile b/Dockerfile index ef54ecc7..3b04fda6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,8 +55,8 @@ RUN mkdir /sdk2 && \ curl -L "https://s3.amazonaws.com/assets.getpebble.com/sdk3/sdk-core/sdk-core-${SDK_TWO_VERSION}.tar.bz2" | \ tar --strip-components=1 -xj -C /sdk2 -ENV SDK_THREE_CHANNEL=beta -ENV SDK_THREE_VERSION=4.0-beta16 +ENV SDK_THREE_CHANNEL=release +ENV SDK_THREE_VERSION=4.0 # Install SDK 3 RUN mkdir /sdk3 && \ diff --git a/bin/post_compile b/bin/post_compile index ba99e9ca..5b44c74c 100644 --- a/bin/post_compile +++ b/bin/post_compile @@ -9,7 +9,7 @@ echo "Downloading SDK 2" curl -o /tmp/pebblesdk2.tar.gz -L https://s3.amazonaws.com/assets.getpebble.com/sdk3/sdk-core/sdk-core-2.9.tar.bz2 echo "Downloading SDK 3" -curl -o /tmp/pebblesdk3.tar.gz -L https://s3.amazonaws.com/assets.getpebble.com/sdk3/beta/sdk-core-4.0-rc20.tar.bz2 +curl -o /tmp/pebblesdk3.tar.gz -L https://s3.amazonaws.com/assets.getpebble.com/sdk3/release/sdk-core-4.0.tar.bz2 echo "Downloading the toolchain" curl -o /tmp/arm-cs-tools.tar https://cloudpebble-vagrant.s3.amazonaws.com/arm-cs-tools-stripped.tar diff --git a/ide/api/project.py b/ide/api/project.py index e0a24c39..3ed62b2f 100644 --- a/ide/api/project.py +++ b/ide/api/project.py @@ -50,6 +50,7 @@ def project_info(request, project_id): 'app_platforms': project.app_platforms, 'app_modern_multi_js': project.app_modern_multi_js, 'menu_icon': project.menu_icon.id if project.menu_icon else None, + 'published_media': project.get_published_media(), 'source_files': [{ 'name': f.file_name, 'id': f.id, @@ -257,11 +258,26 @@ def save_project_dependencies(request, project_id): try: project.set_dependencies(json.loads(request.POST['dependencies'])) project.set_interdependencies([int(x) for x in json.loads(request.POST['interdependencies'])]) + except (IntegrityError, ValueError) as e: + raise BadRequest(str(e)) + else: + send_td_event('cloudpebble_save_dependencies', request=request, project=project) return {'dependencies': project.get_dependencies()} + + +@require_POST +@login_required +@json_view +def save_published_media(request, project_id): + project = get_object_or_404(Project, pk=project_id, owner=request.user) + try: + project.set_published_media(json.loads(request.POST['published_media'])) except (IntegrityError, ValueError) as e: raise BadRequest(str(e)) else: - send_td_event('cloudpebble_save_project_settings', request=request, project=project) + send_td_event('cloudpebble_save_published_media', request=request, project=project) + return {'published_media': project.get_published_media()} + @require_POST @login_required diff --git a/ide/api/ycm.py b/ide/api/ycm.py index f0a17f86..891f6138 100644 --- a/ide/api/ycm.py +++ b/ide/api/ycm.py @@ -7,8 +7,10 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404 from django.views.decorators.http import require_POST +from django.utils.translation import ugettext as _ from urlparse import urlparse + from ide.models.project import Project from utils.jsonview import json_view @@ -38,7 +40,8 @@ def init_autocomplete(request, project_id): 'sdk': request.POST.get('sdk', '2'), 'messagekeys': appkey_names, 'resources': identifiers, - 'dependencies': project.get_dependencies() + 'dependencies': project.get_dependencies(), + 'published_media': [x.name for x in project.published_media.all()] } # Let's go! return _spin_up_server(request) @@ -66,6 +69,7 @@ def _spin_up_server(request): 'server': ws_server, 'secure': secure, 'libraries': response.get('libraries', {}), + 'resources': response.get('resources', []), 'npm_error': response.get('npm_error', None) } diff --git a/ide/migrations/0051_auto__chg_field_resourceidentifier_target_platforms.py b/ide/migrations/0051_auto__chg_field_resourceidentifier_target_platforms.py new file mode 100644 index 00000000..6d6b6e1d --- /dev/null +++ b/ide/migrations/0051_auto__chg_field_resourceidentifier_target_platforms.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Changing field 'ResourceIdentifier.target_platforms' + db.alter_column(u'ide_resourceidentifier', 'target_platforms', self.gf('django.db.models.fields.CharField')(max_length=100, null=True)) + + def backwards(self, orm): + + # Changing field 'ResourceIdentifier.target_platforms' + db.alter_column(u'ide_resourceidentifier', 'target_platforms', self.gf('django.db.models.fields.CharField')(max_length=30, null=True)) + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'ide.buildresult': { + 'Meta': {'object_name': 'BuildResult'}, + 'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'builds'", 'to': "orm['ide.Project']"}), + 'started': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "'db7a8764-aaf8-453b-ba1a-a7a0da137336'", 'max_length': '36'}) + }, + 'ide.buildsize': { + 'Meta': {'object_name': 'BuildSize'}, + 'binary_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sizes'", 'to': "orm['ide.BuildResult']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'resource_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'total_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'worker_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.dependency': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Dependency'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': "orm['ide.Project']"}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) + }, + 'ide.project': { + 'Meta': {'object_name': 'Project'}, + 'app_capabilities': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_company_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_shown_on_communication': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_watchface': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_jshint': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_keys': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'app_keywords': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + 'app_long_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_modern_multi_js': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_platforms': ('django.db.models.fields.TextField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_short_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_uuid': ('django.db.models.fields.CharField', [], {'default': "'72886d2e-9a66-47fc-a852-4044128bacbe'", 'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'app_version_label': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_branch': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'github_hook_build': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'github_hook_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'github_last_commit': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'github_repo': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'optimisation': ('django.db.models.fields.CharField', [], {'default': "'s'", 'max_length': '1'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'project_dependencies': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['ide.Project']", 'symmetrical': 'False'}), + 'project_type': ('django.db.models.fields.CharField', [], {'default': "'native'", 'max_length': '10'}), + 'sdk_version': ('django.db.models.fields.CharField', [], {'default': "'2'", 'max_length': '6'}) + }, + 'ide.resourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'ResourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_menu_icon': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'kind': ('django.db.models.fields.CharField', [], {'max_length': '9'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'resources'", 'to': "orm['ide.Project']"}) + }, + 'ide.resourceidentifier': { + 'Meta': {'object_name': 'ResourceIdentifier'}, + 'character_regex': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'compatibility': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'memory_format': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['ide.ResourceFile']"}), + 'resource_id': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'space_optimisation': ('django.db.models.fields.CharField', [], {'max_length': '7', 'null': 'True', 'blank': 'True'}), + 'storage_format': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'target_platforms': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'tracking': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.resourcevariant': { + 'Meta': {'unique_together': "(('resource_file', 'tags'),)", 'object_name': 'ResourceVariant'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_legacy': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variants'", 'to': "orm['ide.ResourceFile']"}), + 'tags': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '50', 'blank': 'True'}) + }, + 'ide.sourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name', 'target'),)", 'object_name': 'SourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'source_files'", 'to': "orm['ide.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'default': "'app'", 'max_length': '10'}) + }, + 'ide.templateproject': { + 'Meta': {'object_name': 'TemplateProject', '_ormbases': ['ide.Project']}, + u'project_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['ide.Project']", 'unique': 'True', 'primary_key': 'True'}), + 'template_kind': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}) + }, + 'ide.usergithub': { + 'Meta': {'object_name': 'UserGithub'}, + 'avatar': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'github'", 'unique': 'True', 'primary_key': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}) + }, + 'ide.usersettings': { + 'Meta': {'object_name': 'UserSettings'}, + 'accepted_terms': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'autocomplete': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'keybinds': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '20'}), + 'tab_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '2'}), + 'theme': ('django.db.models.fields.CharField', [], {'default': "'cloudpebble'", 'max_length': '50'}), + 'use_spaces': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'whats_new': ('django.db.models.fields.PositiveIntegerField', [], {'default': '24'}) + } + } + + complete_apps = ['ide'] \ No newline at end of file diff --git a/ide/migrations/0052_auto__add_publishedmedia__add_unique_publishedmedia_project_name__add_.py b/ide/migrations/0052_auto__add_publishedmedia__add_unique_publishedmedia_project_name__add_.py new file mode 100644 index 00000000..1ae34753 --- /dev/null +++ b/ide/migrations/0052_auto__add_publishedmedia__add_unique_publishedmedia_project_name__add_.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'PublishedMedia' + db.create_table(u'ide_publishedmedia', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('project', self.gf('django.db.models.fields.related.ForeignKey')(related_name='published_media', to=orm['ide.Project'])), + ('name', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('media_id', self.gf('django.db.models.fields.IntegerField')()), + ('glance', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), + ('has_timeline', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('timeline_tiny', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), + ('timeline_small', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), + ('timeline_large', self.gf('django.db.models.fields.CharField')(max_length=100, blank=True)), + )) + db.send_create_signal('ide', ['PublishedMedia']) + + # Adding unique constraint on 'PublishedMedia', fields ['project', 'name'] + db.create_unique(u'ide_publishedmedia', ['project_id', 'name']) + + # Adding unique constraint on 'PublishedMedia', fields ['project', 'media_id'] + db.create_unique(u'ide_publishedmedia', ['project_id', 'media_id']) + + + def backwards(self, orm): + # Removing unique constraint on 'PublishedMedia', fields ['project', 'media_id'] + db.delete_unique(u'ide_publishedmedia', ['project_id', 'media_id']) + + # Removing unique constraint on 'PublishedMedia', fields ['project', 'name'] + db.delete_unique(u'ide_publishedmedia', ['project_id', 'name']) + + # Deleting model 'PublishedMedia' + db.delete_table(u'ide_publishedmedia') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'ide.buildresult': { + 'Meta': {'object_name': 'BuildResult'}, + 'finished': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'builds'", 'to': "orm['ide.Project']"}), + 'started': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}), + 'state': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'uuid': ('django.db.models.fields.CharField', [], {'default': "'ff465b46-f0da-429e-85ac-6e3fa3ed20e5'", 'max_length': '36'}) + }, + 'ide.buildsize': { + 'Meta': {'object_name': 'BuildSize'}, + 'binary_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'build': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'sizes'", 'to': "orm['ide.BuildResult']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'platform': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'resource_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'total_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'worker_size': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.dependency': { + 'Meta': {'unique_together': "(('project', 'name'),)", 'object_name': 'Dependency'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'dependencies'", 'to': "orm['ide.Project']"}), + 'version': ('django.db.models.fields.CharField', [], {'max_length': '2000'}) + }, + 'ide.project': { + 'Meta': {'object_name': 'Project'}, + 'app_capabilities': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_company_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_is_hidden': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_shown_on_communication': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_is_watchface': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'app_jshint': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_keys': ('django.db.models.fields.TextField', [], {'default': "'{}'"}), + 'app_keywords': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + 'app_long_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_modern_multi_js': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'app_platforms': ('django.db.models.fields.TextField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'app_short_name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'app_uuid': ('django.db.models.fields.CharField', [], {'default': "'0c6dc2fb-3305-4418-9bac-3661c4773ca9'", 'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'app_version_label': ('django.db.models.fields.CharField', [], {'default': "'1.0'", 'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_branch': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'github_hook_build': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'github_hook_uuid': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'github_last_commit': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), + 'github_last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'github_repo': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'optimisation': ('django.db.models.fields.CharField', [], {'default': "'s'", 'max_length': '1'}), + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'project_dependencies': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['ide.Project']", 'symmetrical': 'False'}), + 'project_type': ('django.db.models.fields.CharField', [], {'default': "'native'", 'max_length': '10'}), + 'sdk_version': ('django.db.models.fields.CharField', [], {'default': "'2'", 'max_length': '6'}) + }, + 'ide.publishedmedia': { + 'Meta': {'unique_together': "(('project', 'name'), ('project', 'media_id'))", 'object_name': 'PublishedMedia'}, + 'glance': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'has_timeline': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'media_id': ('django.db.models.fields.IntegerField', [], {}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'published_media'", 'to': "orm['ide.Project']"}), + 'timeline_large': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'timeline_small': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}), + 'timeline_tiny': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}) + }, + 'ide.resourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name'),)", 'object_name': 'ResourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_menu_icon': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'kind': ('django.db.models.fields.CharField', [], {'max_length': '9'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'resources'", 'to': "orm['ide.Project']"}) + }, + 'ide.resourceidentifier': { + 'Meta': {'object_name': 'ResourceIdentifier'}, + 'character_regex': ('django.db.models.fields.CharField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'compatibility': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'memory_format': ('django.db.models.fields.CharField', [], {'max_length': '15', 'null': 'True', 'blank': 'True'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'identifiers'", 'to': "orm['ide.ResourceFile']"}), + 'resource_id': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'space_optimisation': ('django.db.models.fields.CharField', [], {'max_length': '7', 'null': 'True', 'blank': 'True'}), + 'storage_format': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'target_platforms': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'tracking': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}) + }, + 'ide.resourcevariant': { + 'Meta': {'unique_together': "(('resource_file', 'tags'),)", 'object_name': 'ResourceVariant'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_legacy': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'resource_file': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'variants'", 'to': "orm['ide.ResourceFile']"}), + 'tags': ('django.db.models.fields.CommaSeparatedIntegerField', [], {'max_length': '50', 'blank': 'True'}) + }, + 'ide.sourcefile': { + 'Meta': {'unique_together': "(('project', 'file_name', 'target'),)", 'object_name': 'SourceFile'}, + 'file_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'folded_lines': ('django.db.models.fields.TextField', [], {'default': "'[]'"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modified': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'null': 'True', 'blank': 'True'}), + 'project': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'source_files'", 'to': "orm['ide.Project']"}), + 'target': ('django.db.models.fields.CharField', [], {'default': "'app'", 'max_length': '10'}) + }, + 'ide.templateproject': { + 'Meta': {'object_name': 'TemplateProject', '_ormbases': ['ide.Project']}, + u'project_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['ide.Project']", 'unique': 'True', 'primary_key': 'True'}), + 'template_kind': ('django.db.models.fields.IntegerField', [], {'db_index': 'True'}) + }, + 'ide.usergithub': { + 'Meta': {'object_name': 'UserGithub'}, + 'avatar': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '36', 'null': 'True', 'blank': 'True'}), + 'token': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'github'", 'unique': 'True', 'primary_key': 'True', 'to': u"orm['auth.User']"}), + 'username': ('django.db.models.fields.CharField', [], {'max_length': '50', 'null': 'True', 'blank': 'True'}) + }, + 'ide.usersettings': { + 'Meta': {'object_name': 'UserSettings'}, + 'accepted_terms': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'autocomplete': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'keybinds': ('django.db.models.fields.CharField', [], {'default': "'default'", 'max_length': '20'}), + 'tab_width': ('django.db.models.fields.PositiveSmallIntegerField', [], {'default': '2'}), + 'theme': ('django.db.models.fields.CharField', [], {'default': "'cloudpebble'", 'max_length': '50'}), + 'use_spaces': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True', 'primary_key': 'True'}), + 'whats_new': ('django.db.models.fields.PositiveIntegerField', [], {'default': '24'}) + } + } + + complete_apps = ['ide'] \ No newline at end of file diff --git a/ide/models/__init__.py b/ide/models/__init__.py index af9e197d..f9a32907 100644 --- a/ide/models/__init__.py +++ b/ide/models/__init__.py @@ -5,3 +5,4 @@ from ide.models.project import * from ide.models.user import * from ide.models.dependency import * +from ide.models.published_media import * diff --git a/ide/models/files.py b/ide/models/files.py index b24482b5..dabf0216 100644 --- a/ide/models/files.py +++ b/ide/models/files.py @@ -175,7 +175,7 @@ class ResourceIdentifier(IdeModel): character_regex = models.CharField(max_length=100, blank=True, null=True) tracking = models.IntegerField(blank=True, null=True) compatibility = models.CharField(max_length=10, blank=True, null=True) - target_platforms = models.CharField(max_length=30, null=True, blank=True, default=None) + target_platforms = models.CharField(max_length=100, null=True, blank=True, default=None) MEMORY_FORMATS = ( ('Smallest', _('Smallest')), diff --git a/ide/models/project.py b/ide/models/project.py index 63b58f1b..e72ec4d7 100644 --- a/ide/models/project.py +++ b/ide/models/project.py @@ -10,6 +10,7 @@ from ide.models.files import ResourceFile, ResourceIdentifier, SourceFile, ResourceVariant from ide.models.dependency import Dependency from ide.models.meta import IdeModel +from ide.models.published_media import PublishedMedia from ide.utils import generate_half_uuid from ide.utils.regexes import regexes from ide.utils.version import version_to_semver, semver_to_version, parse_sdk_version @@ -90,12 +91,25 @@ def __init__(self, *args, **kwargs): if self.sdk_version == '2': self.app_modern_multi_js = False + def set_published_media(self, published_media): + """ Set the project's publishedMedia entry + :param published_media: A list of publishedMedia dictionaries. + """ + with transaction.atomic(): + self.published_media.all().delete() + for item in published_media: + media = PublishedMedia.from_dict(self, item) + media.save() + + def get_published_media(self): + return [item.to_dict() for item in self.published_media.all()] + def set_dependencies(self, dependencies): """ Set the project's dependencies from a dictionary. :param dependencies: A dictionary of dependency->version """ with transaction.atomic(): - Dependency.objects.filter(project=self).delete() + self.dependencies.all().delete() for name, version in dependencies.iteritems(): dep = Dependency.objects.create(project=self, name=name, version=version) dep.save() diff --git a/ide/models/published_media.py b/ide/models/published_media.py new file mode 100644 index 00000000..2ab62c0a --- /dev/null +++ b/ide/models/published_media.py @@ -0,0 +1,59 @@ +from django.db import models +from django.core.exceptions import ValidationError +from django.utils.translation import ugettext_lazy as _ + +from ide.utils.regexes import regexes +from ide.models.meta import IdeModel + + +class PublishedMedia(IdeModel): + project = models.ForeignKey('Project', related_name='published_media') + name = models.CharField(max_length=100, validators=regexes.validator('c_identifier', _('Invalid identifier'))) + media_id = models.IntegerField() + glance = models.CharField(max_length=100, blank=True, validators=regexes.validator('c_identifier', _('Invalid identifier'))) + has_timeline = models.BooleanField(default=False) + timeline_tiny = models.CharField(max_length=100, blank=True, validators=regexes.validator('c_identifier', _('Invalid identifier'))) + timeline_small = models.CharField(max_length=100, blank=True, validators=regexes.validator('c_identifier', _('Invalid identifier'))) + timeline_large = models.CharField(max_length=100, blank=True, validators=regexes.validator('c_identifier', _('Invalid identifier'))) + + @classmethod + def from_dict(cls, project, data): + return cls.objects.create( + project=project, + name=data['name'], + media_id=data.get('id', None), + glance=data.get('glance', ''), + has_timeline=('timeline' in data), + timeline_tiny=data.get('timeline', {}).get('tiny', ''), + timeline_small=data.get('timeline', {}).get('small', ''), + timeline_large=data.get('timeline', {}).get('large', '') + ) + + + def to_dict(self): + obj = { + 'name': self.name, + 'id': self.media_id + } + if self.glance: + obj['glance'] = self.glance + if self.has_timeline: + obj['timeline'] = { + 'tiny': self.timeline_tiny, + 'small': self.timeline_small, + 'large': self.timeline_large + } + return obj + + def clean(self): + if self.has_timeline and self.glance and self.glance != self.timeline_tiny: + raise ValidationError(_("If glance and timeline.tiny are both used, they must be identical.")) + if self.has_timeline and not (self.timeline_tiny and self.timeline_small and self.timeline_large): + raise ValidationError(_("If timeline icons are enabled, they must all be set.")) + if not self.glance and not self.has_timeline: + raise ValidationError(_("Glance and Timeline cannot both be unset.")) + if self.media_id < 0: + raise ValidationError(_("Published Media IDs cannot be negative.")) + + class Meta(IdeModel.Meta): + unique_together = (('project', 'name'), ('project', 'media_id')) diff --git a/ide/static/ide/css/ide.css b/ide/static/ide/css/ide.css index d534f89f..102b199f 100644 --- a/ide/static/ide/css/ide.css +++ b/ide/static/ide/css/ide.css @@ -527,16 +527,22 @@ table.kv-table .kv-remove { margin-left: 110px; } -.compilation-pane #run-on-phone .btn, -.compilation-pane #run-on-qemu .btn, -.compilation-pane .build-buttons .btn { +.compilation-pane .compilation-buttons.two-buttons .btn { + width: 287px; + margin: 0 4px; +} + +.compilation-pane .compilation-buttons.three-buttons .btn{ + width: 188px; + margin-right: 9px; +} + +.compilation-pane .compilation-buttons.four-buttons .btn{ width: 137px; margin: 0 4px; } -.compilation-pane #run-on-phone .btn:last-of-type, -.compilation-pane #run-on-qemu .btn:last-of-type, -.compilation-pane .build-buttons .btn:last-child { +.compilation-pane .compilation-buttons .btn:last-child { margin-right: 0; } @@ -641,7 +647,7 @@ table.build-results tr.pending .build-state { clear: both; } -.resource-pane input:not([type=checkbox]):not([type=submit]) { +.resource-pane input:not([type=checkbox]):not([type=submit]):not([type=number]) { width: 435px; } @@ -676,6 +682,36 @@ table.build-results tr.pending .build-state { width: 188px; } +/* Published media pane */ + +.media-item:not(:first-child) .help-block { + display: none; +} + +.media-tool-buttons button { + width: initial; + min-width: 130px; + margin-right: 10px; +} + +.sidebar-error::after { + content: "!"; +} + +.sidebar-error { + background: #e5171f; + display: inline-block; + color: white; + width: 22px; + text-align: center; + font-family: serif; + border-radius: 20px; + padding-right: 1px; + margin-top: -1px; +} +.sidebar-error.hide { + display: none; +} /* Dependancies pane */ @@ -1188,7 +1224,7 @@ button#add-filter { display: table; } -.edit-resource-target-platforms-enabled:not(:checked) ~ .edit-resource-targets { +.form-section-toggle:not(:checked) ~ .form-toggleable-section { display: none; } diff --git a/ide/static/ide/js/KVtable.js b/ide/static/ide/js/KVtable.js index 3ec12d2d..7897e64d 100644 --- a/ide/static/ide/js/KVtable.js +++ b/ide/static/ide/js/KVtable.js @@ -52,7 +52,12 @@ CloudPebble.KVTable = function(table_elm, options) { .addClass('kv-value') .attr('type', opts.value_type) .attr('placeholder', key ? null : opts.value_placeholder)), - $('').append($(' @@ -26,14 +26,14 @@

-
+

-
+
@@ -57,7 +57,7 @@

{% trans 'Last build' %}


-
+
{% if project.project_type == 'package'%} diff --git a/ide/templates/ide/project/published_media.html b/ide/templates/ide/project/published_media.html new file mode 100644 index 00000000..6305df3b --- /dev/null +++ b/ide/templates/ide/project/published_media.html @@ -0,0 +1,71 @@ +{% load i18n %} + +
+
+
+
+ +
+
+

{% trans 'Loading...' %}

+
+ +
+ + +
+
+
+ +
+
+ +
+ + {% trans 'This is used in your code and must be a valid C identifier.' %} +
+
+
+ +
+ + {% trans 'Each Published media item must have a unique ID.' %} +
+
+
+ +
+ + {% trans 'This references the image which will be displayed if this Published Media is used as the icon of an AppGlanceSlice.' %} +
+
+
+ +

+ {% trans 'This references the resource images which will be displayed if this Published Media is used for the various sizes of a timeline icon. Tiny, small and large must have sizes of 25x25, 50x50 and 80x80 respectively.' %} + +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
\ No newline at end of file diff --git a/ide/templates/ide/project/resource.html b/ide/templates/ide/project/resource.html index e7996677..070257c1 100644 --- a/ide/templates/ide/project/resource.html +++ b/ide/templates/ide/project/resource.html @@ -11,7 +11,7 @@ {% if project.is_standard_project_type %} - + w {% endif %} @@ -115,11 +115,11 @@
-

+

-
+

{% for platform in supported_platforms %} diff --git a/ide/templates/ide/project/settings.html b/ide/templates/ide/project/settings.html index 8fb2a324..103f1dd7 100644 --- a/ide/templates/ide/project/settings.html +++ b/ide/templates/ide/project/settings.html @@ -28,7 +28,7 @@
-
+
{# Translators: "Aplite" is a platform name and should be left untranslated. #}
@@ -159,7 +159,7 @@
- +
{% trans "A unique identifier for the app. Takes the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where x is any hexadecimal digit and y is one of 8, 9, A, or B" %} @@ -228,8 +228,8 @@
- - + +
diff --git a/ide/tests/test_import_archive.py b/ide/tests/test_import_archive.py index caa6662e..8de493f5 100644 --- a/ide/tests/test_import_archive.py +++ b/ide/tests/test_import_archive.py @@ -180,6 +180,27 @@ def test_import_rocky(self): self.assertEqual(project.source_files.filter(file_name='lib.js', target='common').count(), 1) self.assertEqual(project.source_files.filter(file_name='app.js', target='pkjs').count(), 1) + def test_import_published_media(self): + published_media = [{ + 'name': 'TEST', + 'id': 0, + 'glance': 'TINY', + 'timeline': { + 'tiny': 'TINY', + 'small': 'SMALL', + 'large': 'LARGE', + } + }] + bundle = build_bundle({ + 'src/main.c': '', + 'package.json': make_package(pebble_options={ + 'publishedMedia': published_media + }) + }) + do_import_archive(self.project_id, bundle) + project = Project.objects.get(pk=self.project_id) + self.assertEqual(project.get_published_media(), published_media) + @mock.patch('ide.models.s3file.s3', fake_s3) class TestImportLibrary(CloudpebbleTestCase): diff --git a/ide/tests/test_manifest_generation.py b/ide/tests/test_manifest_generation.py index c2419d81..24e4cfa0 100644 --- a/ide/tests/test_manifest_generation.py +++ b/ide/tests/test_manifest_generation.py @@ -75,3 +75,22 @@ def test_inter_project_dependencies(self): } manifest = generate_manifest(self.project, []) self.check_package_manifest(manifest, package_options={'dependencies': deps}) + + def test_published_media(self): + """ Check that publishedMedia are represented in the manifest """ + published_media = [{ + 'name': 'TEST', + 'id': 0, + 'glance': 'TINY', + 'timeline': { + 'tiny': 'TINY', + 'small': 'SMALL', + 'large': 'LARGE', + } + }] + self.project.set_published_media(published_media) + manifest = generate_manifest(self.project, []) + self.check_package_manifest(manifest, pebble_options={ + 'publishedMedia': published_media + }) + diff --git a/ide/tests/test_project_assembly.py b/ide/tests/test_project_assembly.py index 1c87492f..74258f6b 100644 --- a/ide/tests/test_project_assembly.py +++ b/ide/tests/test_project_assembly.py @@ -118,11 +118,15 @@ def test_native_SDK3_project_with_worker(self): def test_pebblejs(self): """ Check that an pebblejs project looks right """ with self.get_tree(type='pebblejs'): - pass - # We can't really expect anything specific from pebblejs because, in theory, it can change under our feet. - # But we can check things which will always be there: a doc.html and an appinfo.js + self.add_resource('image.png') + self.add_file('blah.js') + # We can't really expect an exact tree layout from pebblejs because, in theory, it can change under our feet. + # But we can check things which will always be there self.assertTrue(self.tree['appinfo.json']) self.assertTrue(self.tree['doc.html']) + self.assertTrue(self.tree['resources']['images']['image.png']) + self.assertTrue(self.tree['src']['js']['app.js']) + self.assertTrue(self.tree['src']['js']['blah.js']) def test_package(self): diff --git a/ide/urls.py b/ide/urls.py index 98565fa9..dbc12c99 100644 --- a/ide/urls.py +++ b/ide/urls.py @@ -5,7 +5,7 @@ from ide.api.phone import ping_phone, check_phone, list_phones, update_phone from ide.api.project import project_info, compile_project, last_build, build_history, build_log, create_project, \ save_project_settings, save_project_dependencies, delete_project, begin_export, import_zip, import_github, do_import_gist, \ - get_projects + get_projects, save_published_media from ide.api.resource import create_resource, resource_info, delete_resource, update_resource, show_resource, \ delete_variant from ide.api.source import create_source_file, load_source_file, source_file_is_safe, save_source_file, \ @@ -28,6 +28,7 @@ url(r'^project/(?P\d+)/info', project_info, name='project_info'), url(r'^project/(?P\d+)/save_settings', save_project_settings, name='save_project_settings'), url(r'^project/(?P\d+)/save_dependencies', save_project_dependencies, name='save_project_dependencies'), + url(r'^project/(?P\d+)/save_published_media', save_published_media, name='save_published_media'), url(r'^project/(?P\d+)/delete', delete_project, name='delete_project'), url(r'^project/(?P\d+)/create_source_file', create_source_file, name='create_source_file'), url(r'^project/(?P\d+)/source/(?P\d+)/load', load_source_file, name='load_source_file'), diff --git a/ide/utils/regexes.py b/ide/utils/regexes.py index 015bafac..c5b97746 100644 --- a/ide/utils/regexes.py +++ b/ide/utils/regexes.py @@ -9,7 +9,7 @@ class RegexHolder(object): # Match major.minor.0, where major and minor are numbers between 0 and 255 with no leading 0s 'semver': r'^(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.(0|[1-9]\d?|1\d{2}|2[0-4]\d|25[0-5])\.0$', - # Match a string of letters and numbers separated with underscores but not starting with a digit + # Match a string of letters and numbers separated with underscores 'c_identifier': r'^\w+$', # Match a C identifier optionally followed by an [array index] diff --git a/ide/utils/sdk/manifest.py b/ide/utils/sdk/manifest.py index e132b8a9..006d41cc 100644 --- a/ide/utils/sdk/manifest.py +++ b/ide/utils/sdk/manifest.py @@ -87,6 +87,9 @@ def generate_v3_manifest_dict(project, resources): manifest['pebble']['displayName'] = project.app_long_name if project.app_is_hidden: manifest['pebble']['watchapp']['hiddenApp'] = project.app_is_hidden + published_media = project.get_published_media() + if published_media: + manifest['pebble']['publishedMedia'] = published_media if project.app_platforms: manifest['pebble']['targetPlatforms'] = project.app_platform_list return manifest @@ -272,6 +275,7 @@ def load_manifest_dict(manifest, manifest_kind, default_project_type='native'): """ project = {} dependencies = {} + published_media = [] if manifest_kind == APPINFO_MANIFEST: project['app_short_name'] = manifest['shortName'] project['app_long_name'] = manifest['longName'] @@ -290,8 +294,10 @@ def load_manifest_dict(manifest, manifest_kind, default_project_type='native'): project['keywords'] = manifest.get('keywords', []) dependencies = manifest.get('dependencies', {}) manifest = manifest['pebble'] + published_media = manifest.get('publishedMedia', []) project['app_modern_multi_js'] = manifest.get('enableMultiJS', True) project['sdk_version'] = manifest.get('sdkVersion', '3') + else: raise InvalidProjectArchiveException(_('Invalid manifest kind: %s') % manifest_kind[-12:]) @@ -308,4 +314,4 @@ def load_manifest_dict(manifest, manifest_kind, default_project_type='native'): else: media_map = {} project['project_type'] = manifest.get('projectType', default_project_type) - return project, media_map, dependencies + return project, media_map, dependencies, published_media diff --git a/ide/utils/sdk/project_assembly.py b/ide/utils/sdk/project_assembly.py index 6634c359..c48c23ff 100644 --- a/ide/utils/sdk/project_assembly.py +++ b/ide/utils/sdk/project_assembly.py @@ -53,12 +53,12 @@ def assemble_resource_directories(project, base_dir): os.makedirs(os.path.join(resource_path, 'data')) -def assemble_resources(resource_path, resources, type_restrictions=None): +def assemble_resources(base_dir, resource_path, resources, type_restrictions=None): """ Copy all the project's resources to a path, optionally filtering by type. """ for f in resources: if type_restrictions and f.kind not in type_restrictions: continue - target_dir = os.path.abspath(os.path.join(resource_path, ResourceFile.DIR_MAP[f.kind])) + target_dir = os.path.abspath(os.path.join(base_dir, resource_path, ResourceFile.DIR_MAP[f.kind])) f.copy_all_variants_to_dir(target_dir) @@ -71,7 +71,7 @@ def assemble_project(project, base_dir, build_result=None): assemble_source_files(project, base_dir) if project.project_type != 'rocky': assemble_resource_directories(project, base_dir) - assemble_resources(os.path.join(base_dir, project.resources_path), resources) + assemble_resources(base_dir, project.resources_path, resources) with open(os.path.join(base_dir, 'wscript'), 'w') as wscript: wscript.write(generate_wscript_file(project)) with open(os.path.join(base_dir, 'pebble-jshintrc'), 'w') as jshint: @@ -84,7 +84,7 @@ def assemble_project(project, base_dir, build_result=None): assemble_resource_directories(project, base_dir) shutil.rmtree(base_dir) shutil.copytree(settings.PEBBLEJS_ROOT, base_dir) - assemble_resources(project.resources_path, resources, type_restrictions=('png', 'bitmap')) + assemble_resources(base_dir, project.resources_path, resources, type_restrictions=('png', 'bitmap')) assemble_source_files(project, base_dir) # All projects have a manifest diff --git a/ide/utils/sdk/sdk_scripts.py b/ide/utils/sdk/sdk_scripts.py index 83c0cf9f..83254f5c 100644 --- a/ide/utils/sdk/sdk_scripts.py +++ b/ide/utils/sdk/sdk_scripts.py @@ -292,15 +292,14 @@ def build(ctx): if build_worker: worker_elf = '{}/pebble-worker.elf'.format(ctx.env.BUILD_DIR) binaries.append({'platform': p, 'app_elf': app_elf, 'worker_elf': worker_elf}) - ctx.pbl_worker(source=ctx.path.ant_glob(['worker_src/c/**/*.c', 'src/js/**/*.json']), target=worker_elf) + ctx.pbl_worker(source=ctx.path.ant_glob('worker_src/c/**/*.c'), target=worker_elf) else: binaries.append({'platform': p, 'app_elf': app_elf}) ctx.set_group('bundle') ctx.pbl_bundle(binaries=binaries, js=ctx.path.ant_glob(['src/pkjs/**/*.js', 'src/pkjs/**/*.json']), js_entry_file='src/pkjs/{{pkjs_entry}}') """ - - return wscript.replace('{{jshint}}', 'True' if jshint and not for_export else 'False').replace('{{pkjs_entry}}', project.pkjs_entry_point) + return wscript.replace('{{jshint}}', 'True' if jshint and not for_export else 'False').replace('{{pkjs_entry}}', project.pkjs_entry_point or '') def generate_wscript_file(project, for_export=False): diff --git a/ide/utils/whatsnew.py b/ide/utils/whatsnew.py index 7990a872..6882c042 100644 --- a/ide/utils/whatsnew.py +++ b/ide/utils/whatsnew.py @@ -45,6 +45,7 @@ ["We now have support for using packages, enabling easier code sharing! Check out
our blog post."], ["You can now develop your own packages in CloudPebble! Select the Pebble Package project type, and then test your package by including it from your other projects in the Dependency pane."], ["Write watchfaces with JavaScript on the Freakin' Watch!"], + ["You can now specify Published Media in CloudPebble. Check out the developer documentation for details."], ]