Merge branch 'master' of git.workaround.org:chaas/debshots

This commit is contained in:
Christoph Haas 2018-05-22 23:14:20 +02:00
commit 181ea83540
29 changed files with 678 additions and 386 deletions

View file

@ -1,37 +0,0 @@
FROM debian:8
MAINTAINER email@christoph-haas.de
# Install apt based dependencies required to run Rails as
# well as RubyGems. As the Ruby image itself is based on a
# Debian image, we use apt-get to install those.
RUN apt-get update && apt-get install -y \
build-essential ruby
RUN apt-get install -y git
RUN apt-get install -y rbenv
# Configure the main working directory. This is the base
# directory used in any further RUN, COPY, and ENTRYPOINT
# commands.
RUN mkdir -p /app
WORKDIR /app
# Copy the Gemfile as well as the Gemfile.lock and install
# the RubyGems. This is a separate step so the dependencies
# will be cached unless changes to one of those two files
# are made.
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install --jobs 20 --retry 5
# Copy the main application.
COPY . ./
# Expose port 3000 to the Docker host, so we can access it
# from the outside.
EXPOSE 3000
# The main command to run when the container starts. Also
# tell the Rails dev server to bind to all interfaces by
# default.
CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]

View file

@ -17,5 +17,3 @@ Install the dependencies:
Install a PostgreSQL server:
apt install postgresql
C

View file

@ -15,7 +15,6 @@
//= require foundation
//= require fancybox
//= require cookies_eu
//= require jquery.amaran
//= require_tree .
$(function(){ $(document).foundation(); });

View file

@ -21,25 +21,44 @@
// 16. Drilldown
// 17. Dropdown
// 18. Dropdown Menu
// 19. Forms
// 20. Label
// 21. Media Object
// 22. Menu
// 23. Meter
// 24. Off-canvas
// 25. Orbit
// 26. Pagination
// 27. Progress Bar
// 28. Responsive Embed
// 29. Reveal
// 30. Slider
// 31. Switch
// 32. Table
// 33. Tabs
// 34. Thumbnail
// 35. Title Bar
// 36. Tooltip
// 37. Top Bar
// 19. Flexbox Utilities
// 20. Forms
// 21. Label
// 22. Media Object
// 23. Menu
// 24. Meter
// 25. Off-canvas
// 26. Orbit
// 27. Pagination
// 28. Progress Bar
// 29. Prototype Arrow
// 30. Prototype Border-Box
// 31. Prototype Border-None
// 32. Prototype Bordered
// 33. Prototype Display
// 34. Prototype Font-Styling
// 35. Prototype List-Style-Type
// 36. Prototype Overflow
// 37. Prototype Position
// 38. Prototype Rounded
// 39. Prototype Separator
// 40. Prototype Shadow
// 41. Prototype Sizing
// 42. Prototype Spacing
// 43. Prototype Text-Decoration
// 44. Prototype Text-Transformation
// 45. Prototype Text-Utilities
// 46. Responsive Embed
// 47. Reveal
// 48. Slider
// 49. Switch
// 50. Table
// 51. Tabs
// 52. Thumbnail
// 53. Title Bar
// 54. Tooltip
// 55. Top Bar
// 56. Xy Grid
@import 'util/util';
@ -67,11 +86,16 @@ $body-font-family: 'Helvetica Neue', Helvetica, Roboto, Arial, sans-serif;
$body-antialiased: true;
$global-margin: 1rem;
$global-padding: 1rem;
$global-position: 1rem;
$global-weight-normal: normal;
$global-weight-bold: bold;
$global-radius: 0;
$global-menu-padding: 0 1rem;
$global-menu-nested-margin: 1rem;
$global-text-direction: ltr;
$global-flexbox: false;
$global-flexbox: true;
$global-prototype-breakpoints: false;
$global-color-pick-contrast-tolerance: 0;
$print-transparent-backgrounds: true;
@include add-foundation-colors;
@ -99,6 +123,7 @@ $grid-column-gutter: (
medium: 30px,
);
$grid-column-align-edge: true;
$grid-column-alias: 'columns';
$block-grid-max: 8;
// 4. Base Typography
@ -210,8 +235,17 @@ $accordion-content-padding: 1rem;
// 8. Accordion Menu
// -----------------
$accordionmenu-padding: $global-menu-padding;
$accordionmenu-nested-margin: $global-menu-nested-margin;
$accordionmenu-submenu-padding: $accordionmenu-padding;
$accordionmenu-arrows: true;
$accordionmenu-arrow-color: $primary-color;
$accordionmenu-item-background: null;
$accordionmenu-border: null;
$accordionmenu-submenu-toggle-background: null;
$accordion-submenu-toggle-border: $accordionmenu-border;
$accordionmenu-submenu-toggle-width: 40px;
$accordionmenu-submenu-toggle-height: $accordionmenu-submenu-toggle-width;
$accordionmenu-arrow-size: 6px;
// 9. Badge
@ -235,11 +269,15 @@ $breadcrumbs-item-color-current: $black;
$breadcrumbs-item-color-disabled: $medium-gray;
$breadcrumbs-item-margin: 0.75rem;
$breadcrumbs-item-uppercase: true;
$breadcrumbs-item-slash: true;
$breadcrumbs-item-separator: true;
$breadcrumbs-item-separator-item: '/';
$breadcrumbs-item-separator-item-rtl: '\\';
$breadcrumbs-item-separator-color: $medium-gray;
// 11. Button
// ----------
$button-font-family: inherit;
$button-padding: 0.85em 1em;
$button-margin: 0 0 $global-margin 0;
$button-fill: solid;
@ -248,6 +286,7 @@ $button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white;
$button-color-alt: $black;
$button-radius: $global-radius;
$button-hollow-border-width: 1px;
$button-sizes: (
tiny: 0.6rem,
small: 0.75rem,
@ -292,7 +331,7 @@ $card-border: 1px solid $light-gray;
$card-shadow: none;
$card-border-radius: $global-radius;
$card-padding: $global-padding;
$card-margin: $global-margin;
$card-margin-bottom: $global-margin;
// 15. Close Button
// ----------------
@ -319,9 +358,13 @@ $closebutton-color-hover: $black;
$drilldown-transition: transform 0.15s linear;
$drilldown-arrows: true;
$drilldown-padding: $global-menu-padding;
$drilldown-nested-margin: 0;
$drilldown-background: $white;
$drilldown-submenu-padding: $drilldown-padding;
$drilldown-submenu-background: $white;
$drilldown-arrow-color: $primary-color;
$drilldown-arrow-size: 6px;
$drilldown-background: $white;
// 17. Dropdown
// ------------
@ -344,11 +387,24 @@ $dropdown-sizes: (
$dropdownmenu-arrows: true;
$dropdownmenu-arrow-color: $anchor-color;
$dropdownmenu-arrow-size: 6px;
$dropdownmenu-arrow-padding: 1.5rem;
$dropdownmenu-min-width: 200px;
$dropdownmenu-background: $white;
$dropdownmenu-submenu-background: $dropdownmenu-background;
$dropdownmenu-padding: $global-menu-padding;
$dropdownmenu-nested-margin: 0;
$dropdownmenu-submenu-padding: $dropdownmenu-padding;
$dropdownmenu-border: 1px solid $medium-gray;
$dropdown-menu-item-color-active: get-color(primary);
$dropdown-menu-item-background-active: transparent;
// 19. Forms
// 19. Flexbox Utilities
// ---------------------
$flex-source-ordering-count: 6;
$flexbox-responsive-breakpoints: true;
// 20. Forms
// ---------
$fieldset-border: 1px solid $medium-gray;
@ -375,11 +431,13 @@ $input-placeholder-color: $medium-gray;
$input-font-family: inherit;
$input-font-size: rem-calc(16);
$input-font-weight: $global-weight-normal;
$input-line-height: $global-lineheight;
$input-background: $white;
$input-background-focus: $white;
$input-background-disabled: $light-gray;
$input-border: 1px solid $medium-gray;
$input-border-focus: 1px solid $dark-gray;
$input-padding: $form-spacing / 2;
$input-shadow: inset 0 1px 2px rgba($black, 0.1);
$input-shadow-focus: 0 0 5px $medium-gray;
$input-cursor-disabled: not-allowed;
@ -388,7 +446,7 @@ $input-number-spinners: true;
$input-radius: $global-radius;
$form-button-radius: $global-radius;
// 20. Label
// 21. Label
// ---------
$label-background: $primary-color;
@ -399,26 +457,28 @@ $label-font-size: 0.8rem;
$label-padding: 0.33333rem 0.5rem;
$label-radius: $global-radius;
// 21. Media Object
// 22. Media Object
// ----------------
$mediaobject-margin-bottom: $global-margin;
$mediaobject-section-padding: $global-padding;
$mediaobject-image-width-stacked: 100%;
// 22. Menu
// 23. Menu
// --------
$menu-margin: 0;
$menu-margin-nested: 1rem;
$menu-item-padding: 0.7rem 1rem;
$menu-nested-margin: $global-menu-nested-margin;
$menu-items-padding: $global-menu-padding;
$menu-simple-margin: 1rem;
$menu-item-color-active: $white;
$menu-item-background-active: get-color(primary);
$menu-icon-spacing: 0.25rem;
$menu-item-background-hover: $light-gray;
$menu-border: $light-gray;
$menu-state-back-compat: true;
$menu-centered-back-compat: true;
// 23. Meter
// 24. Meter
// ---------
$meter-height: 1rem;
@ -428,23 +488,26 @@ $meter-fill-good: $success-color;
$meter-fill-medium: $warning-color;
$meter-fill-bad: $alert-color;
// 24. Off-canvas
// 25. Off-canvas
// --------------
$offcanvas-size: 250px;
$offcanvas-vertical-size: 250px;
$offcanvas-background: $light-gray;
$offcanvas-shadow: 0 0 10px rgba($black, 0.7);
$offcanvas-push-zindex: 1;
$offcanvas-overlap-zindex: 10;
$offcanvas-reveal-zindex: 1;
$offcanvas-inner-shadow-size: 20px;
$offcanvas-inner-shadow-color: rgba($black, 0.25);
$offcanvas-overlay-zindex: 11;
$offcanvas-push-zindex: 12;
$offcanvas-overlap-zindex: 13;
$offcanvas-reveal-zindex: 12;
$offcanvas-transition-length: 0.5s;
$offcanvas-transition-timing: ease;
$offcanvas-fixed-reveal: true;
$offcanvas-exit-background: rgba($white, 0.25);
$maincontent-class: 'off-canvas-content';
// 25. Orbit
// 26. Orbit
// ---------
$orbit-bullet-background: $medium-gray;
@ -459,7 +522,7 @@ $orbit-control-background-hover: rgba($black, 0.5);
$orbit-control-padding: 1rem;
$orbit-control-zindex: 10;
// 26. Pagination
// 27. Pagination
// --------------
$pagination-font-size: rem-calc(14);
@ -477,7 +540,7 @@ $pagination-mobile-items: false;
$pagination-mobile-current-item: false;
$pagination-arrows: true;
// 27. Progress Bar
// 28. Progress Bar
// ----------------
$progress-height: 1rem;
@ -486,7 +549,168 @@ $progress-margin-bottom: $global-margin;
$progress-meter-background: $primary-color;
$progress-radius: $global-radius;
// 28. Responsive Embed
// 29. Prototype Arrow
// -------------------
$prototype-arrow-directions: (
down,
up,
right,
left
);
$prototype-arrow-size: 0.4375rem;
$prototype-arrow-color: $black;
// 30. Prototype Border-Box
// ------------------------
$prototype-border-box-breakpoints: $global-prototype-breakpoints;
// 31. Prototype Border-None
// -------------------------
$prototype-border-none-breakpoints: $global-prototype-breakpoints;
// 32. Prototype Bordered
// ----------------------
$prototype-bordered-breakpoints: $global-prototype-breakpoints;
$prototype-border-width: rem-calc(1);
$prototype-border-type: solid;
$prototype-border-color: $medium-gray;
// 33. Prototype Display
// ---------------------
$prototype-display-breakpoints: $global-prototype-breakpoints;
$prototype-display: (
inline,
inline-block,
block,
table,
table-cell
);
// 34. Prototype Font-Styling
// --------------------------
$prototype-font-breakpoints: $global-prototype-breakpoints;
$prototype-wide-letter-spacing: rem-calc(4);
$prototype-font-normal: $global-weight-normal;
$prototype-font-bold: $global-weight-bold;
// 35. Prototype List-Style-Type
// -----------------------------
$prototype-list-breakpoints: $global-prototype-breakpoints;
$prototype-style-type-unordered: (
disc,
circle,
square
);
$prototype-style-type-ordered: (
decimal,
lower-alpha,
lower-latin,
lower-roman,
upper-alpha,
upper-latin,
upper-roman
);
// 36. Prototype Overflow
// ----------------------
$prototype-overflow-breakpoints: $global-prototype-breakpoints;
$prototype-overflow: (
visible,
hidden,
scroll
);
// 37. Prototype Position
// ----------------------
$prototype-position-breakpoints: $global-prototype-breakpoints;
$prototype-position: (
static,
relative,
absolute,
fixed
);
$prototype-position-z-index: 975;
// 38. Prototype Rounded
// ---------------------
$prototype-rounded-breakpoints: $global-prototype-breakpoints;
$prototype-border-radius: rem-calc(3);
// 39. Prototype Separator
// -----------------------
$prototype-separator-breakpoints: $global-prototype-breakpoints;
$prototype-separator-align: center;
$prototype-separator-height: rem-calc(2);
$prototype-separator-width: 3rem;
$prototype-separator-background: $primary-color;
$prototype-separator-margin-top: $global-margin;
// 40. Prototype Shadow
// --------------------
$prototype-shadow-breakpoints: $global-prototype-breakpoints;
$prototype-box-shadow: 0 2px 5px 0 rgba(0,0,0,.16),
0 2px 10px 0 rgba(0,0,0,.12);
// 41. Prototype Sizing
// --------------------
$prototype-sizing-breakpoints: $global-prototype-breakpoints;
$prototype-sizing: (
width,
height
);
$prototype-sizes: (
25: 25%,
50: 50%,
75: 75%,
100: 100%
);
// 42. Prototype Spacing
// ---------------------
$prototype-spacing-breakpoints: $global-prototype-breakpoints;
$prototype-spacers-count: 3;
// 43. Prototype Text-Decoration
// -----------------------------
$prototype-decoration-breakpoints: $global-prototype-breakpoints;
$prototype-text-decoration: (
overline,
underline,
line-through,
);
// 44. Prototype Text-Transformation
// ---------------------------------
$prototype-transformation-breakpoints: $global-prototype-breakpoints;
$prototype-text-transformation: (
lowercase,
uppercase,
capitalize
);
// 45. Prototype Text-Utilities
// ----------------------------
$prototype-utilities-breakpoints: $global-prototype-breakpoints;
$prototype-text-overflow: ellipsis;
// 46. Responsive Embed
// --------------------
$responsive-embed-margin-bottom: rem-calc(16);
@ -495,7 +719,7 @@ $responsive-embed-ratios: (
widescreen: 16 by 9,
);
// 29. Reveal
// 47. Reveal
// ----------
$reveal-background: $white;
@ -507,7 +731,7 @@ $reveal-radius: $global-radius;
$reveal-zindex: 1005;
$reveal-overlay-background: rgba($black, 0.45);
// 30. Slider
// 48. Slider
// ----------
$slider-width-vertical: 0.5rem;
@ -521,7 +745,7 @@ $slider-handle-background: $primary-color;
$slider-opacity-disabled: 0.25;
$slider-radius: $global-radius;
// 31. Switch
// 49. Switch
// ----------
$switch-background: $medium-gray;
@ -537,7 +761,7 @@ $switch-paddle-offset: 0.25rem;
$switch-paddle-radius: $global-radius;
$switch-paddle-transition: all 0.25s ease-out;
// 32. Table
// 50. Table
// ---------
$table-background: $white;
@ -557,8 +781,9 @@ $table-foot-row-hover: darken($table-foot-background, $table-hover-scale);
$table-head-font-color: $body-font-color;
$table-foot-font-color: $body-font-color;
$show-header-for-stacked: false;
$table-stack-breakpoint: medium;
// 33. Tabs
// 51. Tabs
// --------
$tab-margin: 0;
@ -575,7 +800,7 @@ $tab-content-border: $light-gray;
$tab-content-color: $body-font-color;
$tab-content-padding: 1rem;
// 34. Thumbnail
// 52. Thumbnail
// -------------
$thumbnail-border: solid 4px $white;
@ -585,7 +810,7 @@ $thumbnail-shadow-hover: 0 0 6px 1px rgba($primary-color, 0.5);
$thumbnail-transition: box-shadow 200ms ease-out;
$thumbnail-radius: $global-radius;
// 35. Title Bar
// 53. Title Bar
// -------------
$titlebar-background: $black;
@ -596,20 +821,22 @@ $titlebar-icon-color: $white;
$titlebar-icon-color-hover: $medium-gray;
$titlebar-icon-spacing: 0.25rem;
// 36. Tooltip
// 54. Tooltip
// -----------
$has-tip-cursor: help;
$has-tip-font-weight: $global-weight-bold;
$has-tip-border-bottom: dotted 1px $dark-gray;
$tooltip-background-color: $black;
$tooltip-color: $white;
$tooltip-padding: 0.75rem;
$tooltip-max-width: 10rem;
$tooltip-font-size: $small-font-size;
$tooltip-pip-width: 0.75rem;
$tooltip-pip-height: $tooltip-pip-width * 0.866;
$tooltip-radius: $global-radius;
// 37. Top Bar
// 55. Top Bar
// -----------
$topbar-padding: 0.5rem;
@ -618,3 +845,18 @@ $topbar-submenu-background: $topbar-background;
$topbar-title-spacing: 0.5rem 1rem 0.5rem 0;
$topbar-input-width: 200px;
$topbar-unstack-breakpoint: medium;
// 56. Xy Grid
// -----------
$xy-grid: true;
$grid-container: $global-width;
$grid-columns: 12;
$grid-margin-gutters: (
small: 20px,
medium: 30px
);
$grid-padding-gutters: $grid-margin-gutters;
$grid-container-padding: $grid-padding-gutters;
$grid-container-max: $global-width;
$block-grid-max: 8;

View file

@ -12,8 +12,5 @@
*= require foundation_and_overrides
*= require cookies_eu
*= require fancybox
* Flash/notifications using http://www.amaranjs.com/
* https://github.com/hakanersu/AmaranJS
*= require amaran
*= require font-awesome
*/

View file

@ -370,3 +370,26 @@ a.black
// font-weight: bold;
line-height: 1;
}
.button {
box-shadow: 0px 0px 5px #000000;
border-radius: 10px !important;
}
// Simple animation for FontAwesome icons in callouts at the top.
.fa-wobble {
animation-name: fa-wobble;
animation-duration: 0.5s;
animation-direction: alternate;
animation-iteration-count: infinite;
}
@keyframes fa-wobble {
0% { opacity: 0.2; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
.menu .menu-text {
font-size: 2.5rem;
}

View file

@ -7,13 +7,15 @@ class ApplicationController < ActionController::Base
# filter_parameter_logging :password
before_action :get_current_users_screenshots, :debian_sso
# TODO: Deprecation. What is the replacement for Rails.env.development? ?
before_action :better_errors_hack, if: -> { Rails.env.development? }
# Query for packages that were uploaded by the current user.
# As AAA is not yet implemented it means looking for uploads
# that correspond to the user's cookie token.
# If the user is anonymous then find the uploads by the cookie token.
# If the user is logged in then find the uploads by matching the user id.
def get_current_users_screenshots
if session[:token]
@current_users_screenshots = Screenshot.uploaded_by(session[:token])
if user_signed_in?
@current_users_screenshots = current_user.screenshots
end
end
@ -50,4 +52,11 @@ class ApplicationController < ActionController::Base
def after_sign_in_path_for(resource)
my_welcome_path
end
# better_errors gets extremely slow without this hack
# see: https://github.com/charliesome/better_errors/issues/341
def better_errors_hack
env['puma.config'].options.user_options.delete :app
end
end

View file

@ -1,62 +1,96 @@
class PackagesController < ApplicationController
protect_from_forgery :except => :legacy_uploadfile
def list
@packages = query_packages.paginate(page: params[:page], per_page: 6)
end
def grid
@packages = query_packages.paginate(page: params[:page], per_page: 24)
end
def details
# TODO: Get only screenshots visible to the user (admin or owner or approved)
@package = Package.find_by(name: params[:name])
unless @package
@packagename = params[:name]
render 'notfound'
end
end
# Show upload form for new images
def upload
@package = Package.find_by!(name: params[:name])
end
# POST target of the screenshots upload form.
# Receives uploaded images. Checks if they are valid. Asks for description.
# This action saves the screenshots already if they are valid. The user is
# then given the chance to comment on and delete the screenshots again.
def upload_receive
# Send a cookie to remember the user by the cookie session.
create_user_token
# Create a pseudo user account for the user.
# The user won't know that an account is created.
# But this makes it easier to track who screenshots belong to.
create_pseudo_user unless user_signed_in?
@package = Package.find_by!(name: params[:name])
@valid_images = []
@invalid_images = []
params[:file].each do |img|
params[:file].each do |img|
new_screenshot = @package.screenshots.new(image: img)
# Check if the image was valid
if new_screenshot.valid?
new_screenshot.uploaderhash = session[:token]
new_screenshot.uploaderip = session[:ip]
new_screenshot.version = @package.version
new_screenshot.user = current_user
# TODO: must it be moderated first?
new_screenshot.save
Log.log "Screenshot #{new_screenshot.id} uploaded successfully from #{session[:ip]}. User has token #{session[:token]}"
Log.log "Screenshot #{new_screenshot.id} uploaded successfully from #{session[:ip]}. User has token #{session[:token]} or is #{current_user}"
if current_user.is_admin?
Log.log "Admin upload is automatically approved."
new_screenshot.approve_screenshot!
end
@valid_images.push new_screenshot
else
Log.log "Screenshot #{new_screenshot.image_file_name} invalid (#{new_screenshot.errors[:image]})."
@invalid_images.push new_screenshot
end
end
errors = []
@invalid_images.each do |image|
# errors << "The image #{image.image_file_name} #{image.errors[:image].join(' and ')}."
errors << "The image #{image.image_file_name} is not valid."
flash[:error] = errors.join(" ")
end
# Redirect back to upload form if all uploads were invalid
unless @valid_images.any?
Log.log "No valid images uploaded. Back to upload form."
redirect_to(upload_path, error: errors) and return
end
# Show a list of invalid uploads by default. Or redirect to the review page
# if all uploads were okay.
# redirect_to upload_review_path unless @invalid_images
# TODO
# if @invalid_images…
# ' #{image.image_file_name} (#{image.errors[:image].join(' and ')})
# redirect_to package_path
redirect_to package_path
# render :details
end
# Legacy action to upload an image along with metadata.
# This was used in Debshots 1.x as the default upload method.
# This method allows that old-style way to upload screenshots.
@ -69,9 +103,9 @@ class PackagesController < ApplicationController
# - file
def legacy_uploadfile
@package = Package.find_by!(name: params[:packagename])
new_screenshot = @package.screenshots.new(image: params[:file])
# Check if the image was valid
if new_screenshot.valid?
# new_screenshot.uploaderhash = session[:token]
@ -85,27 +119,30 @@ class PackagesController < ApplicationController
head :not_acceptable
end
end
def delete_screenshot
# Is the user allowed to delete the screenshot?
@screenshot = Screenshot.find(params[:id])
if user_can_alter_screenshot?
# Check if the user is allowed to change this screenshot
# - Is this the user's own screenshot? (anonymous)
if @screenshot.user == current_user or current_user.is_admin?
logger.debug "User #{current_user} deletes screenshot #{@screenshot}"
@screenshot.destroy
flash['notice'] = "Screenshot deleted."
redirect_to :back
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
else
head :forbidden
end
end
def approve_screenshot
@screenshot = Screenshot.find(params[:id])
@screenshot.approve_screenshot!
flash['notice'] = "Screenshot approved."
redirect_to :back
redirect_back(fallback_location: package_path)
end
# Returns a 160x120 thumbnail image if posssible.
# If the package is not found it returns a dummy image along with status 404.
# If the package is found but has no screenshots then it also returns a
@ -116,25 +153,25 @@ class PackagesController < ApplicationController
thumbnail404
return
end
# Called as /thumbnail-with-version/:name/:version
if params[:version]
@screenshot = @package.best_screenshot_for_version(params[:version])
# Called as /thumbnail/:name
# Called as /thumbnail/:name
else
@screenshot = @package.screenshots.first
end
# Return a 404 if the package has no screenshots or the image was not found
unless @screenshot and @screenshot.image.path
thumbnail404
return
end
# Send the thumbnail (uses X-Sendfile or similar if possible)
send_file @screenshot.image.path(:thumb), type: "image/png", disposition: 'inline'
end
# Returns a large screenshot image if posssible.
# If the package is not found it returns a dummy image along with status 404.
# If the package is found but has no screenshots then it also returns a
@ -145,39 +182,41 @@ class PackagesController < ApplicationController
screenshot404
return
end
# Called as /thumbnail-with-version/:name/:version
if params[:version]
@screenshot = @package.best_screenshot_for_version(params[:version])
# Called as /thumbnail/:name
# Called as /thumbnail/:name
else
@screenshot = @package.screenshots.first
end
# Return a 404 if the package has no screenshots or the image was not found
unless @screenshot and @screenshot.image.path
screenshot404
return
end
# Send the thumbnail (uses X-Sendfile or similar if possible)
send_file @screenshot.image.path(:large), type: "image/png", disposition: 'inline'
end
# Receives a form with a simple text field 'description' so that users can update
# the description of their screenshot.
def update_screenshot_description
@screenshot = Screenshot.find(params[:id])
@screenshot.description = params[:description]
@screenshot.save!
flash['notice'] = "Description updated."
redirect_to :back
end
# Receive an anonymous report from a user to have a screenshot removed.
def report_screenshot
@screenshot = Screenshot.find(params[:id])
# if verify_recaptcha
# TODO: Check permissions to do that
@screenshot = Screenshot.find(params[:id])
# @screenshot.description = params[:description]
@screenshot.update params_screenshot_description
@screenshot.save!
flash['notice'] = "Description updated."
redirect_back(fallback_location: package_path(name: @screenshot.package.name))
end
# Receive an anonymous report from a user to have a screenshot removed.
def report_screenshot
@screenshot = Screenshot.find(params[:id])
# if verify_recaptcha
@screenshot.delete_reason = params[:delete_reason]
@screenshot.markedfordelete = true
if @screenshot.valid?
@ -187,46 +226,57 @@ class PackagesController < ApplicationController
errors = @screenshot.errors.to_a.join(' and ')
flash['alert'] = "Sorry. #{errors}"
end
# end
redirect_to :back
end
# Show an HTML partial with reviews of this package from the Ubuntu API
def reviews
expires_in 1.day, public: true
# @reviews = Package.find_by_name!(params[:name]).ubuntu_reviews
@reviews = get_ubuntu_reviews params[:name]
render '_reviews', layout: false
end
private
# Send a dummy thumbnail reading "No screenshot available. Sorry."
def thumbnail404
send_file Rails.root.join('public/images/dummy/thumbnail404.png'),
type: "image/png",
disposition: 'inline',
status: 404
end
def screenshot404
send_file Rails.root.join('public/images/dummy/screenshot404.png'),
type: "image/png",
disposition: 'inline',
status: 404
end
# Return packages matching the criteria given by parameters
def query_packages
packages = Package.includes(:screenshots).order(visits: :desc)
# text search
if params[:search].present?
logger.debug "Searching for #{params[:search]}"
packages = packages.general_search(params[:search])
# end
redirect_back(fallback_location: package_path)
end
case params[:show]
# Show an HTML partial with reviews of this package from the Ubuntu API
def reviews
expires_in 1.day, public: true
# @reviews = Package.find_by_name!(params[:name]).ubuntu_reviews
@reviews = get_ubuntu_reviews params[:name]
render '_reviews', layout: false
end
private
# Seamlessly create a user account for the current client.
# It helps track uploads.
def create_pseudo_user
generated_password = Devise.friendly_token.first(8)
new_user = User.create(
name: 'Anonymous',
password: generated_password)
Log.log "New pseudo user for anonymous upload created: #{new_user}"
sign_in(new_user)
end
# Send a dummy thumbnail reading "No screenshot available. Sorry."
def thumbnail404
send_file Rails.root.join('public/images/dummy/thumbnail404.png'),
type: "image/png",
disposition: 'inline',
status: 404
end
def screenshot404
send_file Rails.root.join('public/images/dummy/screenshot404.png'),
type: "image/png",
disposition: 'inline',
status: 404
end
# Return packages matching the criteria given by parameters
def query_packages
packages = Package.includes(:screenshots).order(visits: :desc)
# text search
if params[:search].present?
logger.debug "Searching for #{params[:search]}"
packages = packages.general_search(params[:search])
end
case params[:show]
when 'with'
packages = packages.with_screenshots
logger.debug 'Limiting packages to those with screenshots'
@ -234,31 +284,23 @@ class PackagesController < ApplicationController
packages = packages.without_screenshots
logger.debug 'Limiting packages to those without screenshots'
end
return packages
end
# Check if the user is allowed to do changed to a screenshot
def user_can_alter_screenshot?
# - Is this the user's own screenshot?
# - Is the user an admin (=logged in)?
@screenshot.uploaderhash == session[:token] or user_signed_in?
end
# Store a random identifier and the client's IP address in the session
# for later identification.
def create_user_token
session[:token] ||= SecureRandom.hex
session[:ip] ||= request.remote_ip
end
# Get reviews of this package from the Ubuntu API
def get_ubuntu_reviews(packagename)
# Use the URL defined in the configuration to get a JSON string
url = Rails.configuration.ubuntu_reviews_api_url % packagename
logger.debug "Loading Ubuntu reviews for package #{packagename} from #{url}"
body = open(url).read
# Turn JSON into a Ruby data structure
json = JSON.parse(body)
@ -269,5 +311,8 @@ class PackagesController < ApplicationController
json = json.sort { |x,y| y['usefulness_total'].to_i <=> x['usefulness_total'].to_i}
return json
end
def params_screenshot_description
params.require(:screenshot).permit(:description)
end
end

View file

@ -3,16 +3,16 @@ module PackagesHelper
# Return a query of all screenshots that the current user may see
# Consists of:
# - approved (public) screenshots
# - screenshots uploaded by the user (determined by cookie session)
# - all screenshots if the user is logged in
# - screenshots uploaded by the user
# - all screenshots if the user is an admin
def screenshots_visible_to_user(package)
if user_signed_in?
if user_signed_in? and current_user.is_admin?
# User is an admin
package.screenshots.order('created_at DESC')
package.screenshots
elsif user_signed_in?
current_user.screenshots
else
package.screenshots.where(
"approved=true OR uploaderhash=?", session[:token]
).order('created_at DESC')
package.screenshots.where(approved: true).order('created_at DESC')
end
end

View file

@ -27,5 +27,6 @@ class Log < ApplicationRecord
logger.error "Could not log to database - validation errors: #{log.errors.to_a}" unless log.valid?
log.save
# log
logger.info "Log.log - #{log.section} - #{log.message}"
end
end

View file

@ -48,7 +48,7 @@ class Package < ApplicationRecord
# Return a query of all approved/public screenshots of this package
def screenshots_approved
self.screenshots.find_by(approved: true)
self.screenshots.where(approved: true)
end
# Return a query of all approved/public screenshots of this package

View file

@ -90,33 +90,8 @@ class Screenshot < ApplicationRecord
end
end
# Check if the user is allowed to change this screenshot
def user_can_alter?
# - Is this the user's own screenshot? (anonymous)
if self.uploaderhash == session[:token]
logger.debug "Screenshot belongs to current anonymous user"
return true
end
# - Is this the user's own screenshot? (anonymous)
if self.user = current_user
logger.debug "Screenshot belongs to user who is currently logged in"
return true
end
# - Is the user an admin (=logged in)?
if current_user.can_admin?
logger.debug "User is an administrator and has super powers"
return true
end
return false
end
# Publish a screenshot from the moderation queue
def approve_screenshot!
self.delete_reason = nil
self.markedfordelete = false
self.approved = true
@ -131,11 +106,6 @@ class Screenshot < ApplicationRecord
self.find_by(approved: true)
end
# Query for screenshots being uploaded by a certain user (by their token)
def self.uploaded_by(token)
self.where(approved: false, uploaderhash: token)
end
# Check whether the user has administrative permissions
def can_admin?
self.admin == 1

View file

@ -32,6 +32,8 @@ class User < ApplicationRecord
'Amazon'
when 'github'
'GitHub'
else
'local authentication'
end
end

View file

@ -3,24 +3,18 @@
- if flash
- flash.each do |name,msg|
- case name
- when 'notice'
- flash_color = 'white'
- flash_bgcolor = 'green'
- when 'alert'
- flash_color = 'white'
- flash_bgcolor = 'red'
- else
- flash_color = 'black'
- flash_bgcolor = 'gray'
javascript:
$.amaran({
'content': {
'message' :'#{msg}',
'color' : '#{flash_color}',
'bgcolor' : '#{flash_bgcolor}'
},
'position' :'bottom right',
'theme' : 'colorful',
'delay' : 7000
});
- when 'timedout'
/ Workaround for Devise sometimes sending "true"
- next
- when 'notice'
.callout.warning data-closable=true
= fa_icon 'info-circle wobble 2x'
strong< = msg
- when 'error'
.callout.alert data-closable=true
= fa_icon 'meh-o wobble 2x'
strong< = msg
- else
.callout.primary data-closable=true
= fa_icon 'info-circle wobble 2x'
strong< = msg

View file

@ -1,14 +1,14 @@
nav.top-bar
.row
.top-bar-left
.top-bar-title
.top-bar
.top-bar-left
ul.menu
li.menu-text
a href='/'
// TODO: Make the logo depend on the virtual host
img src="/images/logos/debian.svg" width="35" alt="Debian logo"
// TODO: Use proper distribution name
' Debian Screenshots >
.top-bar-right
.top-bar-right
ul.dropdown.menu data-dropdown-menu=true
li class=('active' if controller_name=='welcome' and action_name=='home')
a href="/" Home

View file

@ -17,7 +17,7 @@
.small-6.columns
p Please moderate:
- @pending_screenshots.each do |screenshot|
.container.listview.text-center
.container.listview
/ = screenshot.status
a.black.fancybox href=screenshot.image.url(:large, timestamp: false) rel='fancybox-thumb' title=screenshot.caption
= image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption)

View file

@ -13,14 +13,16 @@
/ TODO: submit - save
h2 Your email address
p = current_user.email
p.help-text
' This web site recognizes you by your email address.
' Don't worry - it will not be shown or given to anyone.
h2 Single-sign-on provider
p You logged in using #{current_user.pretty_provider}.
- unless current_user.email.blank?
h2 Your email address
p = current_user.email
p.help-text
' This web site recognizes you by your email address.
' Don't worry - it will not be shown or given to anyone.
- unless current_user.provider.blank?
h2 Single-sign-on provider
p You logged in using #{current_user.pretty_provider}.
h2 Number of screenshots you uploaded
p = current_user.screenshots.count

View file

@ -1,35 +1,33 @@
// Button that reveals a dropdown/modal for moderation/reporting
.button-bar
ul.button-group
- if not screenshot.approved
li
a.button.small.success[
href=approve_screenshot_path(screenshot.id)
method='post'
] Approve screenshot
- elsif screenshot.markedfordelete
li
a.button.small.success[
href=approve_screenshot_path(screenshot.id)
method='post'
] Keep screenshot
button-group
- if user_signed_in?
a.button.small.alert[
href=delete_screenshot_path(screenshot.id)
onclick="return confirm('Really delete the screenshot?');"
] Delete screenshot
- if user_signed_in?
li
a.button.small.alert[
href=delete_screenshot_path(screenshot.id)
onclick="return confirm('Really delete the screenshot?');"
] Delete screenshot
/ Display additional information to admins
p Status: #{screenshot.adminstatus}
p Uploader IP=#{screenshot.uploaderip}
p Uploader Token=#{session[:token]}
p Uploaded #{screenshot.age_days} (#{screenshot.created_at})
- if not screenshot.approved
a.button.small.success[
href=approve_screenshot_path(screenshot.id)
method='post'
] Approve screenshot
- elsif screenshot.markedfordelete
a.button.small.success[
href=approve_screenshot_path(screenshot.id)
method='post'
] Keep screenshot
/ TODO: Move reporting screenshots to an extra page with a form and captcha
/ - else
/ / Allow anonymous users to report inappropriate screenshots
/ = form_tag(report_screenshot_path(screenshot.id))
/ = text_area_tag 'delete_reason', nil, class: 'input-group-field', maxlength: 100, rows: 3, cols: 50
/ = submit_tag 'Request removal', class: 'button alert'
/ Display additional information to admins
p Status: #{screenshot.adminstatus}
p Uploader IP=#{screenshot.uploaderip}
p Uploader Token=#{session[:token]}
p Uploaded #{screenshot.age_days} (#{screenshot.created_at})
/ TODO: Move reporting screenshots to an extra page with a form and captcha
/ - else
/ / Allow anonymous users to report inappropriate screenshots
/ = form_tag(report_screenshot_path(screenshot.id))
/ = text_area_tag 'delete_reason', nil, class: 'input-group-field', maxlength: 100, rows: 3, cols: 50
/ = submit_tag 'Request removal', class: 'button alert'

View file

@ -1,7 +1,7 @@
// TODO: Handle packages with multiple screenshots
a.black href=package_path(name: pkg.name)
div.grid-thumbnail
- if pkg.screenshots.any?
- if pkg.screenshots_approved.any?
// TODO: smarter selection of the most useful screenshot instead of taking the first one
- screenshot = pkg.screenshots.first
= image_tag(screenshot.image.url(:thumb, timestamp: false), alt: screenshot.caption, class: 'thumbnail')

View file

@ -0,0 +1,22 @@
/ Render this callout on top of the package's details page
/ if images have just been uploaded.
- if (@valid_images and @valid_images.any?) or (@invalid_images and @invalid_images.any?)
.row
.callout.warning
- if @invalid_images.any?
= fa_icon 'exclamation-circle 2x pull-left'
p These images were not accepted:
ul
- @invalid_images.each do |image|
li
' #{image.image_file_name} (#{image.errors[:image].join(' and the image ')})
- if @valid_images.any?
= fa_icon 'check-circle 2x pull-left'
p
' #{pluralize(@valid_images.length, 'screenshot')} received successfully.
' Please review the images and add a description for each image. Thanks.

View file

@ -1,13 +1,17 @@
// TODO: Can be deleted?
//
// Button that reveals a dropdown/modal for users (for their own screenshots)
.text-right
button.small.dropdown.warning.button type="button" data-toggle="admin-info-#{screenshot.id}"
'Manage your screenshot
.dropdown-pane data-dropdown=true id="admin-info-#{screenshot.id}"
a.button.small.alert[
href=delete_screenshot_path(screenshot.id)
onclick="return confirm('Really delete the screenshot?');"
] Delete screenshot
p
i =screenshot.status
/ Ask the user to add a description if not yet done
- if screenshot.description.blank?
/ TODO: Allow the user to edit the screenshot's description again?
div
= form_for screenshot, url: update_screenshot_description_path(screenshot) do |f|
label for='description' Please add a short description of this screenshot:
.input-group
span.input-group-label = fa_icon 'edit wobble'
= f.text_field 'description', class: 'input-group-field', maxlength: 80
.input-group-button
= f.submit 'Save', class: 'button success'
/ Offer to delete the screenshot
a.button.small.alert[
href=delete_screenshot_path(screenshot.id)
onclick="return confirm('Really delete your screenshot again?');"
] Delete your screenshot

View file

@ -1,4 +1,6 @@
/ = link_to 'Back', :back, class: 'button small'
/ If images were just uploaded then show upload success/errors
/ in a callout.
= render 'receive_upload'
.row.packagepage
.small-12.columns
@ -10,7 +12,7 @@
- if screenshots_visible_to_user(@package).count > 0
- screenshots_visible_to_user(@package).each do |screenshot|
.row.listview
.text-center
/ .text-center
a.black.fancybox href=screenshot.image.url(:large, timestamp: false) rel='fancybox-thumb' title=screenshot.caption
= image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption)
- if screenshot.description
@ -26,22 +28,9 @@
- if user_signed_in? and current_user.is_admin?
= render(partial: 'admin_dropdown', locals: {screenshot: screenshot})
// or does the screenshot belong to the user (determined by session cookie)
- elsif screenshot.uploaderhash == session[:token]
/ = render(partial: 'user_dropdown', locals: {screenshot: screenshot})
/ Ask the user to add a description if not yet done
- if screenshot.description.blank?
.imgcaption
p Please add a brief description of this screenshot:
= form_tag(update_screenshot_description_path(screenshot.package.name, screenshot.id))
.input-group
= text_field_tag 'description', nil, class: 'input-group-field', maxlength: 80
.input-group-button
=submit_tag 'Save', class: 'button success'
/ Offer to delete the screenshot
a.button.small.alert[
href=delete_screenshot_path(screenshot.id)
onclick="return confirm('Really delete your screenshot again?');"
] Delete your screenshot
- elsif user_signed_in? and screenshot.user == current_user
= render(partial: 'user_dropdown', locals: {screenshot: screenshot})
// or is the user not related to the screenshot and the screenshot is public?
// TODO: Move to a seperate page with a form

View file

@ -19,14 +19,14 @@
.pkgname
=pkg.name
' >
- if pkg.screenshots.any?
- if pkg.screenshots_approved.any?
// TODO: smarter search for the best screenshot instead of taking the first one
- screenshot = pkg.screenshots.first
a.black title=screenshot.caption href=package_path(name: pkg.name)
= image_tag(screenshot.image.url(:large, timestamp: false), alt: screenshot.caption)
- if pkg.screenshots.length > 1
a.black href=package_path(name: pkg.name)
.text-center and #{pluralize(pkg.screenshots.length-1, 'screenshot')} more…
'and #{pluralize(pkg.screenshots.length-1, 'screenshot')} more…
- else
a href=package_path(name: pkg.name)
img.screenshot src="/images/dummy/no-screenshots-upload-one.svg"

View file

@ -1,3 +1,5 @@
= render 'receive_upload'
.row
h1 = "Upload screenshots for #{@package.name}"

View file

@ -0,0 +1,23 @@
h1 Nothing to see here!
/ / Show upload success/errors and load details view.
/ .row
/ .callout
/
/ - if @invalid_images.any?
/ p These images were not accepted:
/
/ ul
/ - @invalid_images.each do |image|
/ li
/ ' #{image.image_file_name} (#{image.errors[:image].join(' and the image ')})
/
/
/
/ - if @valid_images.any?
/ .row
/ p
/ ' #{pluralize(@valid_images.length, 'screenshot')} received successfully.
/ ' Please review the images and add a description for each image. Thanks.
/
/ = render 'packages/details.slim'

View file

@ -1,50 +0,0 @@
.row
h1 Your uploaded images
.row
p
' Thanks for your upload. Please check the images and add
' short one-line descriptions that describe each of the images.
- if @invalid_images.any?
.callout.warning
p
' However the following screenshots were not accepted:
ul
- @invalid_images.each do |image|
li
' #{image.image_file_name} (#{image.errors[:image].join(' and ')})
- if @valid_images.any?
= form_for :screenshot, url: upload_review2_path do |f|
- @valid_images.each do |screenshot|
.row
.small-4.columns
a.black.fancybox href=screenshot.image.url(:large, timestamp: false) title='Uploaded image'
= image_tag(screenshot.image.url(:large, timestamp: false), alt: 'Uploaded image')
.small-8.columns
em Your file name:
p
= screenshot.image_file_name
em Description:
p
= text_field_tag "description-#{screenshot.id}"
em Delete this image?
p
= check_box "delete-#{screenshot.id}", "Delete this image"
.row
button.button type="submit" id="file-submit"
= fa_stacked_icon "check", base: "circle-thin"
' Done
- else
.row
a.button href=upload_path Back to upload form

View file

@ -23,7 +23,6 @@
input.button.postfix type="submit" value="Search"
div.row
hr
div.large-4.medium-12.small-12.columns.text-center
h2 Newest upload
a.black href=package_path(@newest_upload.package.name)
@ -47,7 +46,7 @@ div.row
a href=upload_path(@most_wanted_package.name)
img src="/images/dummy/no-screenshots-upload-one.svg" width="160"
p
' We lack a screenshot for the
' We lack a screenshot for the
strong =>@most_wanted_package.name
' package. Can you
a href=package_path(@most_wanted_package.name) provide one

View file

@ -27,7 +27,7 @@ Rails.application.routes.draw do
post 'upload/:name' => 'packages#upload_receive', as: :upload_receive, name: /[^\/]+/
# TODO: "get" is probably the wrong method to delete a screenshot
get 'delete_screenshot/:id' => 'packages#delete_screenshot', as: :delete_screenshot
post 'update_screenshot_description/:name/:id' => 'packages#update_screenshot_description', as: :update_screenshot_description
patch 'update_screenshot_description/:id' => 'packages#update_screenshot_description', as: :update_screenshot_description
#post 'report_screenshot/:id' => 'packages#report_screenshot', as: :report_screenshot
# TODO: "get" is probably the wrong method to delete a screenshot
get 'approve_screenshot/:id' => 'packages#approve_screenshot', as: :approve_screenshot

60
doc/TODO Normal file
View file

@ -0,0 +1,60 @@
Next tasks
> Before next deployment.
FIXED Auto-approve admin uploads
FIXED Why does weboob-qt show an uploaded image in grid mode if no screenshot is approved?
FIXED Redirect back to upload form if all uploads are invalid
FIXED Improve callout design. No javascript. Better readability. At the top.
Main text on details page is not shown
Remove appearances of "token" everywhere.
Hint the user to add missing descriptions.
Fix upload process.
Fix my/my uploads.
Fix admin menu.
Make sure that the moderation workflow works.
Any upload will trigger the creation of an ad-hoc account.
Screenshots will always be assigned to a user.
After an upload ask the user to add descriptions to the
screenshots. And allow them to delete the screenshots.
Check all code with "current_user" and "user_signed_in?" to
make sure it matches the new concept.
Create a user account called "Anonymous" - via a migration.
All existing screenshots will become uploaded by that user.
Turn moderation into a seperate page showing just the one
screenshot and aks the user for a deletion reason.
Make sure that users can delete their own screenshots but
no other.
Fix formatting of http://localhost:3000/packages/list?search=weboob&show=
(Enumerations etc.)
> After next deployment
Make sure that ad-hoc users are recognized when they visit again.
Devise should recognize them by their session cookie.
Offer anonymous users to become real users to track their uploads.
The screenshots will have to be moved to the new account
and the ad-hoc account can be deleted.
Move ad-hoc/anonymous uploads to the main anonymous user after
a week.
Add tests.