diff --git a/.env.example b/.env.example index b3c252f3..1b438785 100644 --- a/.env.example +++ b/.env.example @@ -35,3 +35,4 @@ PROJECT_GITHUB_REPOSITORY_URL=https://github.com/noisy/steemprojects.com/ PROJECT_SLUG_ON_PAGE=steemprojects STEEM_ACCOUNT= STEEM_POSTING_KEY= +FILESTACK_API_KEY= diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 613aa3d5..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: python -env: - - DATABASE_URL=postgres://postgres:@localhost:5432/oc -python: - - "3.6" -before_install: - - pip install codecov -install: - - pip install -r requirements.txt -before_script: - - psql -U postgres -c 'CREATE DATABASE "oc";' -script: - - coverage run manage.py test --settings=settings.test -after_success: - - codecov diff --git a/compose/postgres/restore.sh b/compose/postgres/restore.sh index f3a27c14..67442940 100644 --- a/compose/postgres/restore.sh +++ b/compose/postgres/restore.sh @@ -38,4 +38,7 @@ fi echo "beginning restore from $1" echo "-------------------------" -pg_restore --verbose -h postgres -U $POSTGRES_USER --clean -d $POSTGRES_USER $BACKUPFILE +EXIT_CODE=0 + +pg_restore --verbose -h postgres -U $POSTGRES_USER --clean -d $POSTGRES_USER $BACKUPFILE || EXIT_CODE=$? +echo $EXIT_CODE diff --git a/fabfile.py b/fabfile.py index 7e1993b5..b790f397 100644 --- a/fabfile.py +++ b/fabfile.py @@ -197,7 +197,7 @@ def restore_db(filename="tmp.sqlc"): remote_filename, )) - docker_compose("run postgres restore {}".format(remote_filename)) + print(docker_compose("run postgres restore {}".format(remote_filename))) docker_compose("run postgres rm /backups/{}".format(remote_filename)) if ENV.name != "local": diff --git a/package/forms.py b/package/forms.py index 4728e44b..9a9350a6 100644 --- a/package/forms.py +++ b/package/forms.py @@ -1,17 +1,29 @@ import itertools +from urllib.error import HTTPError -from package.models import Category, Project, PackageExample, ProjectImage -from package.utils import prepare_thumbnails -from profiles.models import Account - -from django.core.exceptions import ValidationError from django import forms +from django.conf import settings +from django.core.exceptions import ValidationError from django.forms.formsets import formset_factory from django.forms.models import modelformset_factory from django.forms.widgets import Textarea, TextInput +from django.http import HttpResponseForbidden from django.template.defaultfilters import slugify from floppyforms.__future__ import ModelForm +from package.models import Category, Project, PackageExample, ProjectImage +from package.utils import ( + prepare_thumbnails, + download_file, + get_image_name, + get_file_subtype_from_url, + rename_file, + join_path_with_file_name, + delete_file_from_media, + cut_domain_name_from_url, +) +from profiles.models import Account + def package_help_text(): help_text = "" @@ -195,14 +207,76 @@ def save(self, *args, **kwargs): ) -class ProjectImagesFormSet(BaseProjectImagesFormSet): +class ProjectImageUrlForm(forms.Form): - def __init__(self, project, queryset=None, *args, **kwargs): - self.project = project + url = forms.URLField() + project = forms.CharField() - super(ProjectImagesFormSet, self).__init__(queryset=queryset, *args, **kwargs) + def __init__(self, project, user_data, *args, **kwargs): + super(ProjectImageUrlForm, self).__init__(*args, **kwargs) + self.image_path = None + self.absolute_image_path = None + self.delete = False + self.user = user_data + self.fields["url"].widget = forms.HiddenInput() + self.fields["project"].widget = forms.HiddenInput() + self.fields["project"].initial = project + + def clean(self): + cleaned_data = super(ProjectImageUrlForm, self).clean() + image_url = cleaned_data.get("url") + project = cleaned_data.get("project") + project_object = Project.objects.get(id=project) + if not self.user.profile.can_edit_package(project_object): + raise HttpResponseForbidden("permission denied") + self.delete = cleaned_data.get("DELETE") + if self.delete: + try: + self.absolute_image_path = cut_domain_name_from_url(image_url) + except AttributeError: + raise ValidationError("Processing error") + delete_file_from_media(self.absolute_image_path) + self.image_path = self.absolute_image_path.split("/", 1)[-1] + else: + try: + ProjectImage.assert_image(image_url) + except (AttributeError, HTTPError): + raise ValidationError("File is not image") + + image_project_path = join_path_with_file_name("imgs", project) + dest_path = join_path_with_file_name(settings.MEDIA_ROOT, image_project_path) + uuid_name = download_file(image_url, dest_path) + file_type = get_file_subtype_from_url(image_url) + timestamp_name = get_image_name(file_type) + self.absolute_image_path = rename_file(dest_path, uuid_name, timestamp_name) + self.image_path = join_path_with_file_name(image_project_path, timestamp_name) + return cleaned_data + + def save(self, *args, **kwargs): + project = Project.objects.get(id=self.cleaned_data['project']) + if self.delete: + ProjectImage.objects.filter(project=project, img=self.image_path).delete() + else: + ProjectImage.objects.create(project=project, img=self.image_path) + prepare_thumbnails(self.absolute_image_path) + + +BaseProjectImagesUrlFormSet = formset_factory( + form=ProjectImageUrlForm, + can_delete=True, + extra=0, +) + + +class ProjectImagesUrlFormSet(BaseProjectImagesUrlFormSet): + + def __init__(self, project, user_data, *args, **kwargs): + self.project = project + self.user_data = user_data + super(ProjectImagesUrlFormSet, self).__init__(*args, **kwargs) def get_form_kwargs(self, *args, **kwargs): - form_kwargs = super(ProjectImagesFormSet, self).get_form_kwargs(*args, **kwargs) + form_kwargs = super(ProjectImagesUrlFormSet, self).get_form_kwargs(*args, **kwargs) form_kwargs.update({"project": self.project}) + form_kwargs.update({"user_data": self.user_data}) return form_kwargs diff --git a/package/models.py b/package/models.py index c0f0596f..0a1a8880 100644 --- a/package/models.py +++ b/package/models.py @@ -22,7 +22,7 @@ from core.models import BaseModel from package.repos import get_repo_for_repo_url from package.signals import signal_fetch_latest_metadata -from package.utils import get_version, get_pypi_version, normalize_license +from package.utils import get_version, get_pypi_version, normalize_license, get_image_name, get_file_maintype_from_url from profiles.models import Profile, Account repo_url_help_text = settings.PACKAGINATOR_HELP_TEXT['REPO_URL'] @@ -442,7 +442,6 @@ def role_confirmed_by_project_owner(self): role_confirmed_by_account = models.NullBooleanField(_("Role confirmed by team mate"), blank=True, default=None) - class Meta: unique_together = ("account", "project") @@ -452,7 +451,7 @@ def __str__(self): def project_img_path(instance, filename): _, ext = os.path.splitext(filename) - return 'imgs/{}/{}{}'.format(instance.project.pk, int(round(time.time()*1000)), ext) + return 'imgs/{}/{}'.format(instance.project.pk, get_image_name(ext)) class ProjectImage(BaseModel): @@ -479,6 +478,11 @@ def image_tag_thumb(self): def __str__(self): return "Project: {}, Image: {}".format(self.project.name, self.img.name) + @staticmethod + def assert_image(image_url): + main_type = get_file_maintype_from_url(image_url) + return main_type == 'image' + class PackageExample(BaseModel): diff --git a/package/utils.py b/package/utils.py index 5601fd75..0e8373de 100644 --- a/package/utils.py +++ b/package/utils.py @@ -1,14 +1,16 @@ import logging +import os +import time +import urllib +import uuid +from distutils.version import LooseVersion as versioner from os import makedirs from os.path import join, split, exists, splitext -from PIL import Image -from distutils.version import LooseVersion as versioner - -from requests.compat import quote +from PIL import Image from django.conf import settings from django.db import models - +from requests.compat import quote logger = logging.getLogger(__name__) @@ -132,3 +134,59 @@ def crop_image(image, ratio): box = (padding_x, padding_y, x - padding_x, y - padding_y) return image.crop(box) + + +def download_file(file_url, dest_path, file_name=None): + if not file_name: + file_name = generate_unique_file_name() + file_path = join_path_with_file_name(dest_path, file_name) + makedirs(dest_path, exist_ok=True) + urllib.request.urlretrieve(file_url, file_path) + return file_name + + +def get_file_maintype_from_url(file_url): + return urllib.request.urlopen(file_url).info().get_content_maintype() + + +def get_file_subtype_from_url(file_url): + return urllib.request.urlopen(file_url).info().get_content_subtype() + + +def get_image_name(file_type): + return f"{int(round(time.time()*1000))}.{file_type}" + + +def generate_unique_file_name(): + return str(uuid.uuid4()) + + +def rename_file(file_dir, old_name, new_name): + file_path_old = join_path_with_file_name(file_dir, old_name) + file_path_new = join_path_with_file_name(file_dir, new_name) + os.rename(file_path_old, file_path_new) + return file_path_new + + +def join_path_with_file_name(path, file_name): + if not path.endswith('/'): + path += '/' + return f"{path}{file_name}" + + +def split_path_for_path_and_name(file_path): + return file_path.rsplit("/", 1) + + +def cut_domain_name_from_url(file_url): + return file_url.split("/", 3)[-1] + + +def delete_file_from_media(image_path): + folder_path, image_name = split_path_for_path_and_name(image_path) + image_name = image_name.split(".")[0] + for root, dirs, files in os.walk(folder_path): + for file in files: + if file.find(image_name) != -1: + os.remove(join(root, file)) + diff --git a/package/views.py b/package/views.py index 92bdfebe..7481001e 100644 --- a/package/views.py +++ b/package/views.py @@ -15,12 +15,11 @@ from django.utils.html import escape from django.views.decorators.csrf import csrf_exempt - from grid.models import Grid -from package.forms import PackageForm, PackageExampleForm, DocumentationForm, ProjectImagesFormSet +from package.forms import PackageForm, PackageExampleForm, DocumentationForm, ProjectImagesUrlFormSet +from package.forms import TeamMembersFormSet from package.models import Category, Project, PackageExample, ProjectImage, TeamMembership from package.repos import get_all_repos -from package.forms import TeamMembersFormSet from profiles.models import Account, AccountType from searchv2.builders import rebuild_project_search_index @@ -187,7 +186,6 @@ def publish_project(request, slug): return HttpResponseForbidden("permission denied") - @login_required def edit_images(request, slug, template_name="package/images_form.html"): project = get_object_or_404(Project, slug=slug) @@ -195,20 +193,22 @@ def edit_images(request, slug, template_name="package/images_form.html"): return HttpResponseForbidden("permission denied") if request.POST: - formset = ProjectImagesFormSet(data=request.POST, files=request.FILES, project=project,) + formset = ProjectImagesUrlFormSet(data=request.POST, project=project.id, user_data=request.user) else: - formset = ProjectImagesFormSet(project=project, queryset=ProjectImage.objects.filter(project=project)) + formset = ProjectImagesUrlFormSet(project=project.id, user_data=request.user) if formset.is_valid(): - formset.save() - + for form in formset.forms: + form.save() messages.add_message(request, messages.INFO, 'Project updated successfully') return HttpResponseRedirect(reverse("package", kwargs={"slug": project.slug})) return render(request, template_name, { "formset": formset, + "images": ProjectImage.objects.filter(project=project), "package": project, "action": "Save", + "FILESTACK_API_KEY": settings.FILESTACK_API_KEY, }) diff --git a/settings/base.py b/settings/base.py index dba6c542..21f0c711 100644 --- a/settings/base.py +++ b/settings/base.py @@ -541,3 +541,5 @@ CHRONIKER_CHECK_LOCK_FILE = False CHRONIKER_DISABLE_RAW_COMMAND = True CHRONIKER_EMAIL_SENDER = 'Chroniker' + +FILESTACK_API_KEY = environ.get('FILESTACK_API_KEY') diff --git a/static/img/favicon_hiveprojects.ico b/static/img/favicon_hiveprojects.ico new file mode 100644 index 00000000..3b1f0386 Binary files /dev/null and b/static/img/favicon_hiveprojects.ico differ diff --git a/static/img/favicon.ico b/static/img/favicon_steemprojects.ico similarity index 100% rename from static/img/favicon.ico rename to static/img/favicon_steemprojects.ico diff --git a/static/img/logo_hiveprojects.png b/static/img/logo_hiveprojects.png new file mode 100644 index 00000000..a91b539b Binary files /dev/null and b/static/img/logo_hiveprojects.png differ diff --git a/static/scss/new/layout/_footer.scss b/static/scss/new/layout/_footer.scss index 203bae66..a8ad44ed 100644 --- a/static/scss/new/layout/_footer.scss +++ b/static/scss/new/layout/_footer.scss @@ -39,9 +39,10 @@ &__item { margin: 0 10px; + color: $base-footer-color; } &__link { - color: $base-font-color; + color: $base-footer-link-font-color; } } diff --git a/static/scss/new/themes/_hive.scss b/static/scss/new/themes/_hive.scss new file mode 100644 index 00000000..658b27df --- /dev/null +++ b/static/scss/new/themes/_hive.scss @@ -0,0 +1,75 @@ + +$hive-white: #fff; +$hive-white-light: #efeff8; +$hive-white-lighter: #ebebf5; +$hive-red: #e31337; +$hive-black: #000; +$hive-dark-grey: #212529; +$hive-light-grey: #77777b; + +//$steem-red: #bf2155; +$steem-gray-dark: #3e4f5e; +$steem-gray-light: #7e93a0; +$steem-gray-lighter: #d2d2d2; +$steem-blue: #4aa1f1; +$steem-white: #fff; +$steem-aqua-haze: #f0f4f7; + +$logo-font-color: #212529; + +$gen-red: $hive-red; + +$base-background-color: $hive-white-light; +$base-font-color: $hive-black; +$second-font-color: $steem-gray-light; +$third-font-color: $steem-gray-lighter; +$base-link-color: $hive-red; + +//$m-btn-font-color: $base-font-color; // by default +$m-btn-border-color: $hive-red; +$m-btn-background-color: $hive-white; +$m-btn-border-width: 1px; + +//hover +$m-btn-font-color-hover: $hive-white; +$m-btn-border-color-hover: $hive-red; +$m-btn-background-color-hover: $hive-red; + +// Icons +$base-icon-color: $hive-red; + +// Borders +$base-border-style: solid; + +// Backgrounds +$base-hover-background: $steem-aqua-haze; + +// Font +$base-font-family: 'Source Sans Pro', sans-serif; +// Images + +$base-brand-image: "logo_hiveprojects.png"; + +// Forms +$base-form-background-color: $steem-white; +$base-form-background-color-focus: initial; +$base-form-border-color: $steem-gray-lighter; +$base-form-border-color-focus: initial; +$base-form-border-bottom-color-focus: $steem-gray-lighter; + +// Footer +$base-footer-background-color: $hive-dark-grey; +$base-footer-color: $hive-white; +$base-footer-link-font-color: $hive-white; +$base-footer-button-background-color: $hive-white; +$base-footer-button-border-color: $hive-white; + +$base-table-border-color: $steem-gray-lighter; + +$status-unknown: #000000; +$status-live-released: #4aa1f1; +$status-working-prototype-beta: #c44af1; +$status-demo-alpha: #714af1; +$status-concept: #74d919; +$status-abandoned-broken: #ff0000; +$status-out-of-date-retired: #777777; diff --git a/templates/500.html b/templates/500.html index 55ad5990..b639b1e9 100644 --- a/templates/500.html +++ b/templates/500.html @@ -11,7 +11,7 @@ {% include "vendor/ga.html" %} - +
diff --git a/templates/base.html b/templates/base.html index 1853cf00..fffb1cb0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -26,7 +26,7 @@ - + {% block extra_head %}{% endblock %} {% if global_preferences.general__released or request.user.is_superuser %} diff --git a/templates/package/images_form.html b/templates/package/images_form.html index f5e10d45..28a31f52 100644 --- a/templates/package/images_form.html +++ b/templates/package/images_form.html @@ -14,48 +14,43 @@
- We do not have image cropping tool yet. Suggested ratio for images is 16:9 (for example: 640×360, 1024×576, 1280×720, 1920x1080)
- Images with different ratio are scaled with cover property. First image is used on main page.
+ Suggested ratio for images is 16:9 (for example: 640×360, 1024×576, 1280×720, 1920x1080)
+ First image is used on main page.