From: Guillaume Pellerin Date: Thu, 24 Sep 2015 09:55:59 +0000 (+0200) Subject: make docker, uswgi and app config more generic X-Git-Url: https://git.parisson.com/?a=commitdiff_plain;h=d5c6d4d0e55a2a1ba3a3e5aafb04fedb9782036e;p=diggersdigest.git make docker, uswgi and app config more generic --- diff --git a/Dockerfile b/Dockerfile index 482e1de..0bb171a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,14 @@ ENV LANG fr_FR.UTF-8 ENV LANGUAGE fr_FR:fr ENV LC_ALL fr_FR.UTF-8 -RUN mkdir /code +RUN mkdir /opt/app RUN mkdir /opt/src +WORKDIR /opt/app -WORKDIR /code - -ADD requirements.txt /code/ +ADD requirements.txt /opt/app/ RUN pip install -r requirements.txt -ADD requirements-dev.txt /code/ +ADD requirements-dev.txt /opt/app/ RUN pip install -r requirements-dev.txt --src /opt/src -ADD . /code/ +ADD app /opt/app/ diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/deploy/crontab.template b/app/deploy/crontab.template new file mode 100644 index 0000000..c4af8cf --- /dev/null +++ b/app/deploy/crontab.template @@ -0,0 +1,3 @@ +# Poll Twitter every 5 minutes +# Comment-out if you don't use Mezzanine's Twitter app +*/5 * * * * %(user)s %(manage)s poll_twitter diff --git a/app/deploy/gunicorn.conf.py.template b/app/deploy/gunicorn.conf.py.template new file mode 100644 index 0000000..257bb97 --- /dev/null +++ b/app/deploy/gunicorn.conf.py.template @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import multiprocessing + +bind = "unix:%(proj_path)s/gunicorn.sock" +workers = %(num_workers)s +errorlog = "/home/%(user)s/logs/%(proj_name)s_error.log" +loglevel = "error" +proc_name = "%(proj_name)s" diff --git a/app/deploy/local_settings.py.template b/app/deploy/local_settings.py.template new file mode 100644 index 0000000..d8dc9a0 --- /dev/null +++ b/app/deploy/local_settings.py.template @@ -0,0 +1,37 @@ +from __future__ import unicode_literals + +SECRET_KEY = "%(secret_key)s" +NEVERCACHE_KEY = "%(nevercache_key)s" +ALLOWED_HOSTS = [%(domains_python)s] + +DATABASES = { + "default": { + # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". + "ENGINE": "django.db.backends.postgresql_psycopg2", + # DB name or path to database file if using sqlite3. + "NAME": "%(proj_name)s", + # Not used with sqlite3. + "USER": "%(proj_name)s", + # Not used with sqlite3. + "PASSWORD": "%(db_pass)s", + # Set to empty string for localhost. Not used with sqlite3. + "HOST": "127.0.0.1", + # Set to empty string for default. Not used with sqlite3. + "PORT": "", + } +} + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTOCOL", "https") + +CACHE_MIDDLEWARE_SECONDS = 60 + +CACHE_MIDDLEWARE_KEY_PREFIX = "%(proj_name)s" + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", + "LOCATION": "127.0.0.1:11211", + } +} + +SESSION_ENGINE = "django.contrib.sessions.backends.cache" diff --git a/app/deploy/nginx-app.conf b/app/deploy/nginx-app.conf new file mode 100644 index 0000000..df8bc0a --- /dev/null +++ b/app/deploy/nginx-app.conf @@ -0,0 +1,29 @@ +server_tokens off; + +server { + listen 80; + server_name nginx; + charset utf-8; + + access_log /var/log/nginx/app-access.log; + error_log /var/log/nginx/app-error.log; + + # max upload size + client_max_body_size 4096M; # adjust to taste + + # Django media + location /media { + alias /opt/media; # your Django project's media files - amend as required + autoindex on; + } + # Django static + location /static { + alias /opt/static; # your Django project's static files - amend as required + autoindex on; + } + + location / { + uwsgi_pass app:8000; + include /etc/nginx/uwsgi_params; + } +} diff --git a/app/deploy/nginx.conf.template b/app/deploy/nginx.conf.template new file mode 100644 index 0000000..f99430a --- /dev/null +++ b/app/deploy/nginx.conf.template @@ -0,0 +1,55 @@ + +upstream %(proj_name)s { + server unix:%(proj_path)s/gunicorn.sock fail_timeout=0; +} + +server { + + listen 80; + %(ssl_disabled)s listen 443 ssl; + server_name %(domains_nginx)s; + client_max_body_size 10M; + keepalive_timeout 15; + error_log /home/%(user)s/logs/%(proj_name)s_error_nginx.log info; + + %(ssl_disabled)s ssl_certificate conf/%(proj_name)s.crt; + %(ssl_disabled)s ssl_certificate_key conf/%(proj_name)s.key; + %(ssl_disabled)s ssl_session_cache shared:SSL:10m; + %(ssl_disabled)s ssl_session_timeout 10m; + %(ssl_disabled)s ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA; + %(ssl_disabled)s ssl_prefer_server_ciphers on; + + # Deny illegal Host headers + if ($host !~* ^(%(domains_regex)s)$) { + return 444; + } + + location / { + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_pass http://%(proj_name)s; + } + + location /static/ { + root %(proj_path)s; + access_log off; + log_not_found off; + expires 30d; + } + + location /robots.txt { + root %(proj_path)s/static; + access_log off; + log_not_found off; + } + + location /favicon.ico { + root %(proj_path)s/static/img; + access_log off; + log_not_found off; + } + +} diff --git a/app/deploy/start_app.sh b/app/deploy/start_app.sh new file mode 100644 index 0000000..76303bd --- /dev/null +++ b/app/deploy/start_app.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# paths +root='/opt' +sandbox=$root'/app/app' +manage=$sandbox'/manage.py' +wsgi=$sandbox'/wsgi.py' +static=$root'/static/' +media=$root'/media/' + +# waiting for other services +sh $sandbox/deploy/wait.sh + +# django init +python $manage migrate --noinput +python $manage collectstatic --noinput + +# static files auto update +watchmedo shell-command --patterns="*.js;*.css" --recursive \ + --command='python '$manage' collectstatic --noinput' $static & + +# app start +uwsgi --socket :8000 --wsgi-file $wsgi --chdir $sandbox --master --processes 4 --threads 2 --py-autoreload 3 + +#python $manage runserver 0.0.0.0:8000 diff --git a/app/deploy/supervisor.conf.template b/app/deploy/supervisor.conf.template new file mode 100644 index 0000000..5f48e9b --- /dev/null +++ b/app/deploy/supervisor.conf.template @@ -0,0 +1,9 @@ +[program:gunicorn_%(proj_name)s] +command=%(venv_path)s/bin/gunicorn -c gunicorn.conf.py -p gunicorn.pid %(proj_app)s.wsgi:application +directory=%(proj_path)s +user=%(user)s +autostart=true +stdout_logfile = /home/%(user)s/logs/%(proj_name)s_supervisor +autorestart=true +redirect_stderr=true +environment=LANG="%(locale)s",LC_ALL="%(locale)s",LC_LANG="%(locale)s" diff --git a/app/deploy/wait.sh b/app/deploy/wait.sh new file mode 100644 index 0000000..a3341e0 --- /dev/null +++ b/app/deploy/wait.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +apt-get install -y --force-yes netcat + +set -e + +host=$(env | grep _TCP_ADDR | cut -d = -f 2) +port=$(env | grep _TCP_PORT | cut -d = -f 2) + +echo -n "waiting for TCP connection to $host:$port..." + +while ! nc -w 1 $host $port 2>/dev/null +do + echo -n . + sleep 1 +done + +echo 'ok' \ No newline at end of file diff --git a/app/diggersdigest/__init__.py b/app/diggersdigest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/diggersdigest/local_settings.py b/app/diggersdigest/local_settings.py new file mode 100644 index 0000000..188f580 --- /dev/null +++ b/app/diggersdigest/local_settings.py @@ -0,0 +1,65 @@ + +DEBUG = True + +# Make these unique, and don't share it with anybody. +SECRET_KEY = "+3b01&_6_m@@yb4f06$s0zno8vkybh81nbuj_q(xzk+xeih1+s" +NEVERCACHE_KEY = "l11tr%#!uc@+%$51(&+%=&z6h9yrw42(jpcj$3_&6evtu6hl%z" + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', + 'NAME': 'diggersdigest', + 'USER': 'digger', + 'PASSWORD': 'admin', + 'HOST': 'db', + 'PORT': 3306, + } +} + +# EXTENSIONS AND FORMATS +# Allowed Extensions for File Upload. Lower case is important. +FILEBROWSER_EXTENSIONS = { + 'Folder': [''], + 'Image': ['.jpg', '.jpeg', '.gif', '.png', '.tif', '.tiff'], + 'Document': ['.pdf', '.doc', '.rtf', '.txt', '.xls', '.csv', '.docx'], + 'Video': ['.mov', '.wmv', '.mpeg', '.mpg', '.avi', '.rm'], + 'Audio': ['.mp3', '.mp4', '.wav', '.aiff', '.midi', '.m4p'] + } +# Define different formats for allowed selections. +# This has to be a subset of EXTENSIONS. +# e.g., add ?type=image to the browse-URL ... +FILEBROWSER_SELECT_FORMATS = { + 'File': ['Folder', 'Document'], + 'Image': ['Image'], + 'Media': ['Video', 'Audio'], + 'Audio': ['Audio'], + 'Document': ['Document'], + # for TinyMCE we can also define lower-case items + 'image': ['Image'], + 'file': ['Folder', 'Image', 'Document'], + 'media': ['Video', 'Audio'], + 'audio': ['Audio'], +} + +################### +# DEPLOY SETTINGS # +################### + +# Domains for public site +# ALLOWED_HOSTS = [""] + +# These settings are used by the default fabfile.py provided. +# Check fabfile.py for defaults. + +# FABRIC = { +# "DEPLOY_TOOL": "rsync", # Deploy with "git", "hg", or "rsync" +# "SSH_USER": "", # VPS SSH username +# "HOSTS": [""], # The IP address of your VPS +# "DOMAINS": ALLOWED_HOSTS, # Edit domains in ALLOWED_HOSTS +# "REQUIREMENTS_PATH": "requirements.txt", # Project's pip requirements +# "LOCALE": "en_US.UTF-8", # Should end with ".UTF-8" +# "DB_PASS": "", # Live database password +# "ADMIN_PASS": "", # Live admin user password +# "SECRET_KEY": SECRET_KEY, +# "NEVERCACHE_KEY": NEVERCACHE_KEY, +# } diff --git a/app/diggersdigest/migrations/__init__.py b/app/diggersdigest/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/diggersdigest/migrations/blog/0001_initial.py b/app/diggersdigest/migrations/blog/0001_initial.py new file mode 100644 index 0000000..1124f8b --- /dev/null +++ b/app/diggersdigest/migrations/blog/0001_initial.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import mezzanine.core.fields +import mezzanine.utils.models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('sites', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BlogCategory', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=500, verbose_name='Title')), + ('slug', models.CharField(help_text='Leave blank to have the URL auto-generated from the title.', max_length=2000, null=True, verbose_name='URL', blank=True)), + ('site', models.ForeignKey(editable=False, to='sites.Site')), + ], + options={ + 'ordering': ('title',), + 'verbose_name': 'Blog Category', + 'verbose_name_plural': 'Blog Categories', + }, + bases=(models.Model,), + ), + migrations.CreateModel( + name='BlogPost', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('comments_count', models.IntegerField(default=0, editable=False)), + ('keywords_string', models.CharField(max_length=500, editable=False, blank=True)), + ('rating_count', models.IntegerField(default=0, editable=False)), + ('rating_sum', models.IntegerField(default=0, editable=False)), + ('rating_average', models.FloatField(default=0, editable=False)), + ('title', models.CharField(max_length=500, verbose_name='Title')), + ('slug', models.CharField(help_text='Leave blank to have the URL auto-generated from the title.', max_length=2000, null=True, verbose_name='URL', blank=True)), + ('_meta_title', models.CharField(help_text='Optional title to be used in the HTML title tag. If left blank, the main title field will be used.', max_length=500, null=True, verbose_name='Title', blank=True)), + ('description', models.TextField(verbose_name='Description', blank=True)), + ('gen_description', models.BooleanField(default=True, help_text='If checked, the description will be automatically generated from content. Uncheck if you want to manually set a custom description.', verbose_name='Generate description')), + ('created', models.DateTimeField(null=True, editable=False)), + ('updated', models.DateTimeField(null=True, editable=False)), + ('status', models.IntegerField(default=2, help_text='With Draft chosen, will only be shown for admin users on the site.', verbose_name='Status', choices=[(1, 'Draft'), (2, 'Published')])), + ('publish_date', models.DateTimeField(help_text="With Published chosen, won't be shown until this time", null=True, verbose_name='Published from', blank=True)), + ('expiry_date', models.DateTimeField(help_text="With Published chosen, won't be shown after this time", null=True, verbose_name='Expires on', blank=True)), + ('short_url', models.URLField(null=True, blank=True)), + ('in_sitemap', models.BooleanField(default=True, verbose_name='Show in sitemap')), + ('content', mezzanine.core.fields.RichTextField(verbose_name='Content')), + ('allow_comments', models.BooleanField(default=True, verbose_name='Allow comments')), + ('featured_image', mezzanine.core.fields.FileField(max_length=255, null=True, verbose_name='Featured Image', blank=True)), + ('categories', models.ManyToManyField(related_name='blogposts', verbose_name='Categories', to='blog.BlogCategory', blank=True)), + ('related_posts', models.ManyToManyField(related_name='related_posts_rel_+', verbose_name='Related posts', to='blog.BlogPost', blank=True)), + ('site', models.ForeignKey(editable=False, to='sites.Site')), + ('user', models.ForeignKey(related_name='blogposts', verbose_name='Author', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-publish_date',), + 'verbose_name': 'Blog post', + 'verbose_name_plural': 'Blog posts', + }, + bases=(models.Model, mezzanine.utils.models.AdminThumbMixin), + ), + ] diff --git a/app/diggersdigest/migrations/blog/0002_auto_20150527_1555.py b/app/diggersdigest/migrations/blog/0002_auto_20150527_1555.py new file mode 100644 index 0000000..a5cb65c --- /dev/null +++ b/app/diggersdigest/migrations/blog/0002_auto_20150527_1555.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='blogpost', + name='publish_date', + field=models.DateTimeField(help_text="With Published chosen, won't be shown until this time", null=True, verbose_name='Published from', db_index=True, blank=True), + ), + ] diff --git a/app/diggersdigest/migrations/blog/0003_blogpost_related_products.py b/app/diggersdigest/migrations/blog/0003_blogpost_related_products.py new file mode 100644 index 0000000..3ae1fc5 --- /dev/null +++ b/app/diggersdigest/migrations/blog/0003_blogpost_related_products.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0003_auto_20150906_1911'), + ('blog', '0002_auto_20150527_1555'), + ] + + operations = [ + migrations.AddField( + model_name='blogpost', + name='related_products', + field=models.ManyToManyField(to='shop.Product', verbose_name=b'Related products', blank=True), + ), + ] diff --git a/app/diggersdigest/migrations/blog/__init__.py b/app/diggersdigest/migrations/blog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/diggersdigest/migrations/shop/0001_initial.py b/app/diggersdigest/migrations/shop/0001_initial.py new file mode 100644 index 0000000..d8a505f --- /dev/null +++ b/app/diggersdigest/migrations/shop/0001_initial.py @@ -0,0 +1,284 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from decimal import Decimal +import mezzanine.utils.models +import django.db.models.deletion +import mezzanine.core.fields +import cartridge.shop.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('pages', '0003_auto_20150527_1555'), + ('sites', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('last_updated', models.DateTimeField(null=True, verbose_name='Last updated')), + ], + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('sku', cartridge.shop.fields.SKUField(max_length=20, verbose_name='SKU')), + ('description', models.CharField(max_length=2000, verbose_name='Description')), + ('quantity', models.IntegerField(default=0, verbose_name='Quantity')), + ('unit_price', cartridge.shop.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=10, blank=True, null=True, verbose_name='Unit price')), + ('total_price', cartridge.shop.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=10, blank=True, null=True, verbose_name='Total price')), + ('url', models.CharField(max_length=2000)), + ('image', models.CharField(max_length=200, null=True)), + ('cart', models.ForeignKey(related_name='items', to='shop.Cart')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Category', + fields=[ + ('page_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='pages.Page')), + ('content', mezzanine.core.fields.RichTextField(verbose_name='Content')), + ('featured_image', mezzanine.core.fields.FileField(max_length=255, null=True, verbose_name='Featured Image', blank=True)), + ('price_min', cartridge.shop.fields.MoneyField(null=True, verbose_name='Minimum price', max_digits=10, decimal_places=2, blank=True)), + ('price_max', cartridge.shop.fields.MoneyField(null=True, verbose_name='Maximum price', max_digits=10, decimal_places=2, blank=True)), + ('combined', models.BooleanField(default=True, help_text='If checked, products must match all specified filters, otherwise products can match any specified filter.', verbose_name='Combined')), + ], + options={ + 'ordering': ('_order',), + 'verbose_name': 'Product category', + 'verbose_name_plural': 'Product categories', + }, + bases=('pages.page', models.Model), + ), + migrations.CreateModel( + name='DiscountCode', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=100, verbose_name='Title')), + ('active', models.BooleanField(default=False, verbose_name='Active')), + ('discount_deduct', cartridge.shop.fields.MoneyField(null=True, verbose_name='Reduce by amount', max_digits=10, decimal_places=2, blank=True)), + ('discount_percent', cartridge.shop.fields.PercentageField(null=True, verbose_name='Reduce by percent', max_digits=5, decimal_places=2, blank=True)), + ('discount_exact', cartridge.shop.fields.MoneyField(null=True, verbose_name='Reduce to amount', max_digits=10, decimal_places=2, blank=True)), + ('valid_from', models.DateTimeField(null=True, verbose_name='Valid from', blank=True)), + ('valid_to', models.DateTimeField(null=True, verbose_name='Valid to', blank=True)), + ('code', cartridge.shop.fields.DiscountCodeField(unique=True, max_length=20, verbose_name='Code')), + ('min_purchase', cartridge.shop.fields.MoneyField(null=True, verbose_name='Minimum total purchase', max_digits=10, decimal_places=2, blank=True)), + ('free_shipping', models.BooleanField(default=False, verbose_name='Free shipping')), + ('uses_remaining', models.IntegerField(help_text='If you wish to limit the number of times a code may be used, set this value. It will be decremented upon each use.', null=True, verbose_name='Uses remaining', blank=True)), + ('categories', models.ManyToManyField(related_name='discountcode_related', verbose_name='Categories', to='shop.Category', blank=True)), + ], + options={ + 'verbose_name': 'Discount code', + 'verbose_name_plural': 'Discount codes', + }, + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('billing_detail_first_name', models.CharField(max_length=100, verbose_name='First name')), + ('billing_detail_last_name', models.CharField(max_length=100, verbose_name='Last name')), + ('billing_detail_street', models.CharField(max_length=100, verbose_name='Street')), + ('billing_detail_city', models.CharField(max_length=100, verbose_name='City/Suburb')), + ('billing_detail_state', models.CharField(max_length=100, verbose_name='State/Region')), + ('billing_detail_postcode', models.CharField(max_length=10, verbose_name='Zip/Postcode')), + ('billing_detail_country', models.CharField(max_length=100, verbose_name='Country')), + ('billing_detail_phone', models.CharField(max_length=20, verbose_name='Phone')), + ('billing_detail_email', models.EmailField(max_length=254, verbose_name='Email')), + ('shipping_detail_first_name', models.CharField(max_length=100, verbose_name='First name')), + ('shipping_detail_last_name', models.CharField(max_length=100, verbose_name='Last name')), + ('shipping_detail_street', models.CharField(max_length=100, verbose_name='Street')), + ('shipping_detail_city', models.CharField(max_length=100, verbose_name='City/Suburb')), + ('shipping_detail_state', models.CharField(max_length=100, verbose_name='State/Region')), + ('shipping_detail_postcode', models.CharField(max_length=10, verbose_name='Zip/Postcode')), + ('shipping_detail_country', models.CharField(max_length=100, verbose_name='Country')), + ('shipping_detail_phone', models.CharField(max_length=20, verbose_name='Phone')), + ('additional_instructions', models.TextField(verbose_name='Additional instructions', blank=True)), + ('time', models.DateTimeField(auto_now_add=True, verbose_name='Time', null=True)), + ('key', models.CharField(max_length=40)), + ('user_id', models.IntegerField(null=True, blank=True)), + ('shipping_type', models.CharField(max_length=50, verbose_name='Shipping type', blank=True)), + ('shipping_total', cartridge.shop.fields.MoneyField(null=True, verbose_name='Shipping total', max_digits=10, decimal_places=2, blank=True)), + ('tax_type', models.CharField(max_length=50, verbose_name='Tax type', blank=True)), + ('tax_total', cartridge.shop.fields.MoneyField(null=True, verbose_name='Tax total', max_digits=10, decimal_places=2, blank=True)), + ('item_total', cartridge.shop.fields.MoneyField(null=True, verbose_name='Item total', max_digits=10, decimal_places=2, blank=True)), + ('discount_code', cartridge.shop.fields.DiscountCodeField(max_length=20, verbose_name='Discount code', blank=True)), + ('discount_total', cartridge.shop.fields.MoneyField(null=True, verbose_name='Discount total', max_digits=10, decimal_places=2, blank=True)), + ('total', cartridge.shop.fields.MoneyField(null=True, verbose_name='Order total', max_digits=10, decimal_places=2, blank=True)), + ('transaction_id', models.CharField(max_length=255, null=True, verbose_name='Transaction ID', blank=True)), + ('status', models.IntegerField(default=1, verbose_name='Status', choices=[(1, 'Unprocessed'), (2, 'Processed')])), + ('site', models.ForeignKey(editable=False, to='sites.Site')), + ], + options={ + 'ordering': ('-id',), + 'verbose_name': 'Order', + 'verbose_name_plural': 'Orders', + }, + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('sku', cartridge.shop.fields.SKUField(max_length=20, verbose_name='SKU')), + ('description', models.CharField(max_length=2000, verbose_name='Description')), + ('quantity', models.IntegerField(default=0, verbose_name='Quantity')), + ('unit_price', cartridge.shop.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=10, blank=True, null=True, verbose_name='Unit price')), + ('total_price', cartridge.shop.fields.MoneyField(decimal_places=2, default=Decimal('0'), max_digits=10, blank=True, null=True, verbose_name='Total price')), + ('order', models.ForeignKey(related_name='items', to='shop.Order')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('keywords_string', models.CharField(max_length=500, editable=False, blank=True)), + ('rating_count', models.IntegerField(default=0, editable=False)), + ('rating_sum', models.IntegerField(default=0, editable=False)), + ('rating_average', models.FloatField(default=0, editable=False)), + ('title', models.CharField(max_length=500, verbose_name='Title')), + ('slug', models.CharField(help_text='Leave blank to have the URL auto-generated from the title.', max_length=2000, null=True, verbose_name='URL', blank=True)), + ('_meta_title', models.CharField(help_text='Optional title to be used in the HTML title tag. If left blank, the main title field will be used.', max_length=500, null=True, verbose_name='Title', blank=True)), + ('description', models.TextField(verbose_name='Description', blank=True)), + ('gen_description', models.BooleanField(default=True, help_text='If checked, the description will be automatically generated from content. Uncheck if you want to manually set a custom description.', verbose_name='Generate description')), + ('created', models.DateTimeField(null=True, editable=False)), + ('updated', models.DateTimeField(null=True, editable=False)), + ('status', models.IntegerField(default=2, help_text='With Draft chosen, will only be shown for admin users on the site.', verbose_name='Status', choices=[(1, 'Draft'), (2, 'Published')])), + ('publish_date', models.DateTimeField(help_text="With Published chosen, won't be shown until this time", null=True, verbose_name='Published from', db_index=True, blank=True)), + ('expiry_date', models.DateTimeField(help_text="With Published chosen, won't be shown after this time", null=True, verbose_name='Expires on', blank=True)), + ('short_url', models.URLField(null=True, blank=True)), + ('in_sitemap', models.BooleanField(default=True, verbose_name='Show in sitemap')), + ('content', mezzanine.core.fields.RichTextField(verbose_name='Content')), + ('unit_price', cartridge.shop.fields.MoneyField(null=True, verbose_name='Unit price', max_digits=10, decimal_places=2, blank=True)), + ('sale_id', models.IntegerField(null=True)), + ('sale_price', cartridge.shop.fields.MoneyField(null=True, verbose_name='Sale price', max_digits=10, decimal_places=2, blank=True)), + ('sale_from', models.DateTimeField(null=True, verbose_name='Sale start', blank=True)), + ('sale_to', models.DateTimeField(null=True, verbose_name='Sale end', blank=True)), + ('sku', cartridge.shop.fields.SKUField(max_length=20, unique=True, null=True, verbose_name='SKU', blank=True)), + ('num_in_stock', models.IntegerField(null=True, verbose_name='Number in stock', blank=True)), + ('available', models.BooleanField(default=False, verbose_name='Available for purchase')), + ('image', models.CharField(max_length=100, null=True, verbose_name='Image', blank=True)), + ('date_added', models.DateTimeField(auto_now_add=True, verbose_name='Date added', null=True)), + ('categories', models.ManyToManyField(to='shop.Category', verbose_name='Product categories', blank=True)), + ('related_products', models.ManyToManyField(related_name='related_products_rel_+', verbose_name='Related products', to='shop.Product', blank=True)), + ('site', models.ForeignKey(editable=False, to='sites.Site')), + ('upsell_products', models.ManyToManyField(related_name='upsell_products_rel_+', verbose_name='Upsell products', to='shop.Product', blank=True)), + ], + options={ + 'verbose_name': 'Product', + 'verbose_name_plural': 'Products', + }, + bases=(models.Model, mezzanine.utils.models.AdminThumbMixin), + ), + migrations.CreateModel( + name='ProductAction', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('timestamp', models.IntegerField()), + ('total_cart', models.IntegerField(default=0)), + ('total_purchase', models.IntegerField(default=0)), + ('product', models.ForeignKey(related_name='actions', to='shop.Product')), + ], + ), + migrations.CreateModel( + name='ProductImage', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('_order', mezzanine.core.fields.OrderField(null=True, verbose_name='Order')), + ('file', mezzanine.core.fields.FileField(max_length=255, verbose_name='Image')), + ('description', models.CharField(max_length=100, verbose_name='Description', blank=True)), + ('product', models.ForeignKey(related_name='images', to='shop.Product')), + ], + options={ + 'ordering': ('_order',), + 'verbose_name': 'Image', + 'verbose_name_plural': 'Images', + }, + ), + migrations.CreateModel( + name='ProductOption', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('type', models.IntegerField(verbose_name='Type', choices=[(1, 'Cover condition'), (2, 'Vinyl condition')])), + ('name', cartridge.shop.fields.OptionField(max_length=50, null=True, verbose_name='Name')), + ], + options={ + 'verbose_name': 'Product option', + 'verbose_name_plural': 'Product options', + }, + ), + migrations.CreateModel( + name='ProductVariation', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('unit_price', cartridge.shop.fields.MoneyField(null=True, verbose_name='Unit price', max_digits=10, decimal_places=2, blank=True)), + ('sale_id', models.IntegerField(null=True)), + ('sale_price', cartridge.shop.fields.MoneyField(null=True, verbose_name='Sale price', max_digits=10, decimal_places=2, blank=True)), + ('sale_from', models.DateTimeField(null=True, verbose_name='Sale start', blank=True)), + ('sale_to', models.DateTimeField(null=True, verbose_name='Sale end', blank=True)), + ('sku', cartridge.shop.fields.SKUField(max_length=20, unique=True, null=True, verbose_name='SKU', blank=True)), + ('num_in_stock', models.IntegerField(null=True, verbose_name='Number in stock', blank=True)), + ('default', models.BooleanField(default=False, verbose_name='Default')), + ('option1', cartridge.shop.fields.OptionField(max_length=50, null=True, verbose_name='Cover condition')), + ('option2', cartridge.shop.fields.OptionField(max_length=50, null=True, verbose_name='Vinyl condition')), + ('image', models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Image', blank=True, to='shop.ProductImage', null=True)), + ('product', models.ForeignKey(related_name='variations', to='shop.Product')), + ], + options={ + 'ordering': ('-default',), + }, + ), + migrations.CreateModel( + name='Sale', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=100, verbose_name='Title')), + ('active', models.BooleanField(default=False, verbose_name='Active')), + ('discount_deduct', cartridge.shop.fields.MoneyField(null=True, verbose_name='Reduce by amount', max_digits=10, decimal_places=2, blank=True)), + ('discount_percent', cartridge.shop.fields.PercentageField(null=True, verbose_name='Reduce by percent', max_digits=5, decimal_places=2, blank=True)), + ('discount_exact', cartridge.shop.fields.MoneyField(null=True, verbose_name='Reduce to amount', max_digits=10, decimal_places=2, blank=True)), + ('valid_from', models.DateTimeField(null=True, verbose_name='Valid from', blank=True)), + ('valid_to', models.DateTimeField(null=True, verbose_name='Valid to', blank=True)), + ('categories', models.ManyToManyField(related_name='sale_related', verbose_name='Categories', to='shop.Category', blank=True)), + ('products', models.ManyToManyField(to='shop.Product', verbose_name='Products', blank=True)), + ], + options={ + 'verbose_name': 'Sale', + 'verbose_name_plural': 'Sales', + }, + ), + migrations.AddField( + model_name='discountcode', + name='products', + field=models.ManyToManyField(to='shop.Product', verbose_name='Products', blank=True), + ), + migrations.AddField( + model_name='category', + name='options', + field=models.ManyToManyField(related_name='product_options', verbose_name='Product options', to='shop.ProductOption', blank=True), + ), + migrations.AddField( + model_name='category', + name='products', + field=models.ManyToManyField(to='shop.Product', verbose_name='Products', blank=True), + ), + migrations.AddField( + model_name='category', + name='sale', + field=models.ForeignKey(verbose_name='Sale', blank=True, to='shop.Sale', null=True), + ), + migrations.AlterUniqueTogether( + name='productaction', + unique_together=set([('product', 'timestamp')]), + ), + ] diff --git a/app/diggersdigest/migrations/shop/0002_order_callback_uuid.py b/app/diggersdigest/migrations/shop/0002_order_callback_uuid.py new file mode 100644 index 0000000..34c9955 --- /dev/null +++ b/app/diggersdigest/migrations/shop/0002_order_callback_uuid.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='order', + name='callback_uuid', + field=models.CharField(default=0, max_length=36), + preserve_default=False, + ), + ] diff --git a/app/diggersdigest/migrations/shop/0003_auto_20150906_1911.py b/app/diggersdigest/migrations/shop/0003_auto_20150906_1911.py new file mode 100644 index 0000000..ff51ffb --- /dev/null +++ b/app/diggersdigest/migrations/shop/0003_auto_20150906_1911.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import cartridge.shop.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0002_order_callback_uuid'), + ] + + operations = [ + migrations.AlterField( + model_name='productoption', + name='type', + field=models.IntegerField(verbose_name='Type', choices=[(1, 'Size'), (2, 'Colour')]), + ), + migrations.AlterField( + model_name='productvariation', + name='option1', + field=cartridge.shop.fields.OptionField(max_length=50, null=True, verbose_name='Size'), + ), + migrations.AlterField( + model_name='productvariation', + name='option2', + field=cartridge.shop.fields.OptionField(max_length=50, null=True, verbose_name='Colour'), + ), + ] diff --git a/app/diggersdigest/migrations/shop/__init__.py b/app/diggersdigest/migrations/shop/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/diggersdigest/settings.py b/app/diggersdigest/settings.py new file mode 100644 index 0000000..ffbe234 --- /dev/null +++ b/app/diggersdigest/settings.py @@ -0,0 +1,462 @@ + +from __future__ import absolute_import, unicode_literals +import os +from django.utils.translation import ugettext_lazy as _ + + +###################### +# MEZZANINE SETTINGS # +###################### + +# The following settings are already defined with default values in +# the ``defaults.py`` module within each of Mezzanine's apps, but are +# common enough to be put here, commented out, for conveniently +# overriding. Please consult the settings documentation for a full list +# of settings Mezzanine implements: +# http://mezzanine.jupo.org/docs/configuration.html#default-settings + +# Controls the ordering and grouping of the admin menu. +# +# ADMIN_MENU_ORDER = ( +# ("Content", ("pages.Page", "blog.BlogPost", +# "generic.ThreadedComment", (_("Media Library"), "fb_browse"),)), +# (_("Shop"), ("shop.Product", "shop.ProductOption", "shop.DiscountCode", +# "shop.Sale", "shop.Order")), +# ("Site", ("sites.Site", "redirects.Redirect", "conf.Setting")), +# ("Users", ("auth.User", "auth.Group",)), +# ) + +# A three item sequence, each containing a sequence of template tags +# used to render the admin dashboard. +# +# DASHBOARD_TAGS = ( +# ("blog_tags.quick_blog", "mezzanine_tags.app_list"), +# ("comment_tags.recent_comments",), +# ("mezzanine_tags.recent_actions",), +# ) + +# A sequence of templates used by the ``page_menu`` template tag. Each +# item in the sequence is a three item sequence, containing a unique ID +# for the template, a label for the template, and the template path. +# These templates are then available for selection when editing which +# menus a page should appear in. Note that if a menu template is used +# that doesn't appear in this setting, all pages will appear in it. + +PAGE_MENU_TEMPLATES = ( + (1, _("Top navigation bar"), "pages/menus/dropdown.html"), + # (2, _("Left-hand tree"), "pages/menus/tree.html"), + # (3, _("Footer"), "pages/menus/footer.html"), +) + +# A sequence of fields that will be injected into Mezzanine's (or any +# library's) models. Each item in the sequence is a four item sequence. +# The first two items are the dotted path to the model and its field +# name to be added, and the dotted path to the field class to use for +# the field. The third and fourth items are a sequence of positional +# args and a dictionary of keyword args, to use when creating the +# field instance. When specifying the field class, the path +# ``django.models.db.`` can be omitted for regular Django model fields. +# + +EXTRA_MODEL_FIELDS = ( + ( + # Dotted path to field.# Dotted path to field. + "mezzanine.blog.models.BlogPost.related_products", + # Dotted path to field class. + "django.db.models.ManyToManyField", + # Positional args for field class. + ("shop.Product",), + # Keyword args for field class. + {"verbose_name": ("Related products"), "blank": True}, + ), + # Add the callback_uuid field to orders. This field is helpful for identifying + # orders being checked out. + ( + "cartridge.shop.models.Order.callback_uuid", + "django.db.models.CharField", + (), + {"blank" : False, "max_length" : 36}, + ), + # ... + # # Example of adding a field to *all* of Mezzanine's content types: + # ( + # "mezzanine.pages.models.Page.another_field", + # "IntegerField", # 'django.db.models.' is implied if path is omitted. + # (_("Another name"),), + # {"blank": True, "default": 1}, + # ), + ) + +# Setting to turn on featured images for blog posts. Defaults to False. +# +BLOG_USE_FEATURED_IMAGE = True + +# If True, the django-modeltranslation will be added to the +# INSTALLED_APPS setting. +USE_MODELTRANSLATION = False + +SEARCH_MODEL_CHOICES = ('shop.Product',) + + +###################### +# CARTRIDGE SETTINGS # +###################### + +# The following settings are already defined in cartridge.shop.defaults +# with default values, but are common enough to be put here, commented +# out, for conveniently overriding. Please consult the settings +# documentation for a full list of settings Cartridge implements: +# http://cartridge.jupo.org/configuration.html#default-settings + +# Sequence of available credit card types for payment. +# SHOP_CARD_TYPES = ("Mastercard", "Visa", "Diners", "Amex") + +# Setting to turn on featured images for shop categories. Defaults to False. +# SHOP_CATEGORY_USE_FEATURED_IMAGE = True + +# Set an alternative OrderForm class for the checkout process. +# SHOP_CHECKOUT_FORM_CLASS = 'cartridge.shop.forms.OrderForm' + +# If True, the checkout process is split into separate +# billing/shipping and payment steps. +# SHOP_CHECKOUT_STEPS_SPLIT = True + +# If True, the checkout process has a final confirmation step before +# completion. +# SHOP_CHECKOUT_STEPS_CONFIRMATION = True + +# Controls the formatting of monetary values accord to the locale +# module in the python standard library. If an empty string is +# used, will fall back to the system's locale. +#SHOP_CURRENCY_LOCALE = '' + +# Dotted package path and name of the function that +# is called on submit of the billing/shipping checkout step. This +# is where shipping calculation can be performed and set using the +# function ``cartridge.shop.utils.set_shipping``. +# SHOP_HANDLER_BILLING_SHIPPING = \ +# "cartridge.shop.checkout.default_billship_handler" + +# Dotted package path and name of the function that +# is called once an order is successful and all of the order +# object's data has been created. This is where any custom order +# processing should be implemented. +# SHOP_HANDLER_ORDER = "cartridge.shop.checkout.default_order_handler" + +# Dotted package path and name of the function that +# is called on submit of the payment checkout step. This is where +# integration with a payment gateway should be implemented. +# SHOP_HANDLER_PAYMENT = "cartridge.shop.checkout.default_payment_handler" + +# Sequence of value/name pairs for order statuses. +# SHOP_ORDER_STATUS_CHOICES = ( +# (1, "Unprocessed"), +# (2, "Processed"), +# ) + +# Sequence of value/name pairs for types of product options, +# eg Size, Colour. NOTE: Increasing the number of these will +# require database migrations! +#SHOP_OPTION_TYPE_CHOICES = ( +# (1, "Cover condition"), +# (2, "Vinyl condition"), +# ) + +SHOP_USE_VARIATIONS = False + +# Sequence of indexes from the SHOP_OPTION_TYPE_CHOICES setting that +# control how the options should be ordered in the admin, +# SHOP_OPTION_ADMIN_ORDER = (1, 2) + +SHOP_USE_RATINGS = False + +# Add Migration Module path see : https://github.com/stephenmcd/mezzanine/blob/master/docs/model-customization.rst#field-injection-caveats +MIGRATION_MODULES = { + "shop": "diggersdigest.migrations.shop", + "blog": "diggersdigest.migrations.blog" +} + +# USE or EXTEND the custom callback-uuid form +SHOP_CHECKOUT_FORM_CLASS = 'payments.multipayments.forms.base.CallbackUUIDOrderForm' + +PRIMARY_PAYMENT_PROCESSOR_IN_USE = False + +SECONDARY_PAYMENT_PROCESSORS = ( + ('paypal', { + 'name' : 'Pay With Pay-Pal', + 'form' : 'payments.multipayments.forms.paypal.PaypalSubmissionForm' + }), +) + + +# Currency type. +PAYPAL_CURRENCY = "EUR" + +# Business account email. Sandbox emails look like this. +PAYPAL_BUSINESS = 'pellerin@parisson.com' +PAYPAL_RECEIVER_EMAIL = PAYPAL_BUSINESS + +# Use this to enable https on return URLs. This is strongly recommended! (Except for sandbox) +PAYPAL_RETURN_WITH_HTTPS = False + +# Function that returns args for `reverse`. +# URL is sent to PayPal as the for returning to a 'complete' landing page. +PAYPAL_RETURN_URL = lambda cart, uuid, order_form: ('shop_complete', None, None) + +# Function that returns args for `reverse`. +# URL is sent to PayPal as the URL to callback to for PayPal IPN. +# Set to None if you do not wish to use IPN. +PAYPAL_IPN_URL = lambda cart, uuid, order_form: ('paypal.standard.ipn.views.ipn', None, {}) + + +# URL the secondary-payment-form is submitted to +# Dev example +PAYPAL_SUBMIT_URL = 'https://www.sandbox.paypal.com/cgi-bin/webscr' +# Prod example +# PAYPAL_SUBMIT_URL = 'https://www.paypal.com/cgi-bin/webscr' + +# For real use set to False +PAYPAL_TEST = True + +######################## +# MAIN DJANGO SETTINGS # +######################## + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = ['*'] + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'UTC' + +# If you set this to True, Django will use timezone-aware datetimes. +USE_TZ = True + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = "fr" + +# Supported languages +LANGUAGES = ( + ('fr', _('French')), + ('en', _('English')), +) + +# A boolean that turns on/off debug mode. When set to ``True``, stack traces +# are displayed for error pages. Should always be set to ``False`` in +# production. Best set to ``True`` in local_settings.py +DEBUG = False + +# Whether a user's session cookie expires when the Web browser is closed. +SESSION_EXPIRE_AT_BROWSER_CLOSE = True + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +AUTHENTICATION_BACKENDS = ("mezzanine.core.auth_backends.MezzanineBackend",) + +# The numeric mode to set newly-uploaded files to. The value should be +# a mode you'd pass directly to os.chmod. +FILE_UPLOAD_PERMISSIONS = 0o644 + + + +############# +# DATABASES # +############# + +DATABASES = { + "default": { + # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". + "ENGINE": "django.db.backends.sqlite3", + # DB name or path to database file if using sqlite3. + "NAME": "dev.db", + # Not used with sqlite3. + "USER": "", + # Not used with sqlite3. + "PASSWORD": "", + # Set to empty string for localhost. Not used with sqlite3. + "HOST": "", + # Set to empty string for default. Not used with sqlite3. + "PORT": "", + } +} + +######### +# PATHS # +######### + +# Full filesystem path to the project. +PROJECT_APP_PATH = os.path.dirname(os.path.abspath(__file__)) +PROJECT_APP = os.path.basename(PROJECT_APP_PATH) +PROJECT_ROOT = BASE_DIR = os.path.dirname(PROJECT_APP_PATH) + +# Every cache key will get prefixed with this value - here we set it to +# the name of the directory the project is in to try and use something +# project specific. +CACHE_MIDDLEWARE_KEY_PREFIX = PROJECT_APP + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = "/static/" + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +# STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/")) +STATIC_ROOT = '/opt/static/' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = "/media/" + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +# MEDIA_ROOT = os.path.join(PROJECT_ROOT, *MEDIA_URL.strip("/").split("/")) +MEDIA_ROOT = '/opt/media/' + +# Package/module name to import the root urlpatterns from for the project. +ROOT_URLCONF = "%s.urls" % PROJECT_APP + +# Put strings here, like "/home/html/django_templates" +# or "C:/www/django/templates". +# Always use forward slashes, even on Windows. +# Don't forget to use absolute paths, not relative paths. +TEMPLATE_DIRS = (os.path.join(PROJECT_ROOT, "templates"),) + + +################ +# APPLICATIONS # +################ + +INSTALLED_APPS = ( + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.redirects", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.sitemaps", + "django.contrib.staticfiles", + "mezzanine.boot", + "mezzanine.conf", + "mezzanine.core", + "mezzanine.generic", + "mezzanine.pages", + "cartridge.shop", + "mezzanine.blog", + "mezzanine.forms", + "mezzanine.galleries", + "mezzanine.twitter", + "mezzanine.accounts", + # "mezzanine.mobile", + "records", + "payments.multipayments", + 'paypal.standard.ipn', +) + +# List of processors used by RequestContext to populate the context. +# Each one should be a callable that takes the request object as its +# only parameter and returns a dictionary to add to the context. +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.static", + "django.core.context_processors.media", + "django.core.context_processors.request", + "django.core.context_processors.tz", + "mezzanine.conf.context_processors.settings", + "mezzanine.pages.context_processors.page", + "payments.multipayments.context_processors.settings", +) + +# List of middleware classes to use. Order is important; in the request phase, +# these middleware classes will be applied in the order given, and in the +# response phase the middleware will be applied in reverse order. +MIDDLEWARE_CLASSES = ( + "mezzanine.core.middleware.UpdateCacheMiddleware", + + 'django.contrib.sessions.middleware.SessionMiddleware', + # Uncomment if using internationalisation or localisation + # 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', + + "cartridge.shop.middleware.ShopMiddleware", + "mezzanine.core.request.CurrentRequestMiddleware", + "mezzanine.core.middleware.RedirectFallbackMiddleware", + "mezzanine.core.middleware.TemplateForDeviceMiddleware", + "mezzanine.core.middleware.TemplateForHostMiddleware", + "mezzanine.core.middleware.AdminLoginInterfaceSelectorMiddleware", + "mezzanine.core.middleware.SitePermissionMiddleware", + # Uncomment the following if using any of the SSL settings: + # "mezzanine.core.middleware.SSLRedirectMiddleware", + "mezzanine.pages.middleware.PageMiddleware", + "mezzanine.core.middleware.FetchFromCacheMiddleware", +) + +# Store these package names here as they may change in the future since +# at the moment we are using custom forks of them. +PACKAGE_NAME_FILEBROWSER = "filebrowser_safe" +PACKAGE_NAME_GRAPPELLI = "grappelli_safe" + +######################### +# OPTIONAL APPLICATIONS # +######################### + +# These will be added to ``INSTALLED_APPS``, only if available. +OPTIONAL_APPS = ( + "debug_toolbar", + "django_extensions", + "compressor", + PACKAGE_NAME_FILEBROWSER, + PACKAGE_NAME_GRAPPELLI, +) + +################## +# LOCAL SETTINGS # +################## + +# Allow any settings to be defined in local_settings.py which should be +# ignored in your version control system allowing for settings to be +# defined per machine. +try: + from .local_settings import * +except ImportError as e: + if "local_settings" not in str(e): + raise e + + +#################### +# DYNAMIC SETTINGS # +#################### + +# set_dynamic_settings() will rewrite globals based on what has been +# defined so far, in order to provide some better defaults where +# applicable. We also allow this settings module to be imported +# without Mezzanine installed, as the case may be when using the +# fabfile, where setting the dynamic settings below isn't strictly +# required. +try: + from mezzanine.utils.conf import set_dynamic_settings +except ImportError: + pass +else: + set_dynamic_settings(globals()) diff --git a/app/diggersdigest/urls.py b/app/diggersdigest/urls.py new file mode 100644 index 0000000..6efe25e --- /dev/null +++ b/app/diggersdigest/urls.py @@ -0,0 +1,111 @@ +from __future__ import unicode_literals + +from django.conf.urls import patterns, include, url +from django.conf.urls.i18n import i18n_patterns +from django.contrib import admin + +from mezzanine.core.views import direct_to_template +from mezzanine.conf import settings + + +admin.autodiscover() + +# Add the urlpatterns for any custom Django applications here. +# You can also change the ``home`` view to add your own functionality +# to the project's homepage. + +urlpatterns = i18n_patterns("", + # Change the admin prefix here to use an alternate URL for the + # admin interface, which would be marginally more secure. + ("^admin/", include(admin.site.urls)), +) + +if settings.USE_MODELTRANSLATION: + urlpatterns += patterns('', + url('^i18n/$', 'django.views.i18n.set_language', name='set_language'), + ) + +urlpatterns += patterns('', + + # Cartridge URLs. + ("^shop/", include("cartridge.shop.urls")), + url("^account/orders/$", "cartridge.shop.views.order_history", + name="shop_order_history"), + + # We don't want to presume how your homepage works, so here are a + # few patterns you can use to set it up. + + # HOMEPAGE AS STATIC TEMPLATE + # --------------------------- + # This pattern simply loads the index.html template. It isn't + # commented out like the others, so it's the default. You only need + # one homepage pattern, so if you use a different one, comment this + # one out. + + url("^$", direct_to_template, {"template": "index.html"}, name="home"), + + # HOMEPAGE AS AN EDITABLE PAGE IN THE PAGE TREE + # --------------------------------------------- + # This pattern gives us a normal ``Page`` object, so that your + # homepage can be managed via the page tree in the admin. If you + # use this pattern, you'll need to create a page in the page tree, + # and specify its URL (in the Meta Data section) as "/", which + # is the value used below in the ``{"slug": "/"}`` part. Make + # sure to uncheck all templates for the "show in menus" field + # when you create the page, since the link to the homepage is + # always hard-coded into all the page menus that display navigation + # on the site. Also note that the normal rule of adding a custom + # template per page with the template name using the page's slug + # doesn't apply here, since we can't have a template called + # "/.html" - so for this case, the template "pages/index.html" can + # be used. + + # url("^$", "mezzanine.pages.views.page", {"slug": "/"}, name="home"), + + # HOMEPAGE FOR A BLOG-ONLY SITE + # ----------------------------- + # This pattern points the homepage to the blog post listing page, + # and is useful for sites that are primarily blogs. If you use this + # pattern, you'll also need to set BLOG_SLUG = "" in your + # ``settings.py`` module, and delete the blog page object from the + # page tree in the admin if it was installed. + + # url("^$", "mezzanine.blog.views.blog_post_list", name="home"), + + # MEZZANINE'S URLS + # ---------------- + # ADD YOUR OWN URLPATTERNS *ABOVE* THE LINE BELOW. + # ``mezzanine.urls`` INCLUDES A *CATCH ALL* PATTERN + # FOR PAGES, SO URLPATTERNS ADDED BELOW ``mezzanine.urls`` + # WILL NEVER BE MATCHED! + + # If you'd like more granular control over the patterns in + # ``mezzanine.urls``, go right ahead and take the parts you want + # from it, and use them directly below instead of using + # ``mezzanine.urls``. + ("^", include("mezzanine.urls")), + + # MOUNTING MEZZANINE UNDER A PREFIX + # --------------------------------- + # You can also mount all of Mezzanine's urlpatterns under a + # URL prefix if desired. When doing this, you need to define the + # ``SITE_PREFIX`` setting, which will contain the prefix. Eg: + # SITE_PREFIX = "my/site/prefix" + # For convenience, and to avoid repeating the prefix, use the + # commented out pattern below (commenting out the one above of course) + # which will make use of the ``SITE_PREFIX`` setting. Make sure to + # add the import ``from django.conf import settings`` to the top + # of this file as well. + # Note that for any of the various homepage patterns above, you'll + # need to use the ``SITE_PREFIX`` setting as well. + + # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls")) + + (r'^paypal-ipn-8c5erc9ye49ia51rn655mi4xs7/', include('paypal.standard.ipn.urls')), + +) + +# Adds ``STATIC_URL`` to the context of error pages, so that error +# pages can use JS, CSS and images. +handler404 = "mezzanine.core.views.page_not_found" +handler500 = "mezzanine.core.views.server_error" diff --git a/app/manage.py b/app/manage.py new file mode 100644 index 0000000..32f1de8 --- /dev/null +++ b/app/manage.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + + from mezzanine.utils.conf import real_project_name + + settings_module = "%s.settings" % real_project_name("diggersdigest") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/app/records/__init__.py b/app/records/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/records/admin.py b/app/records/admin.py new file mode 100644 index 0000000..b629d13 --- /dev/null +++ b/app/records/admin.py @@ -0,0 +1,87 @@ +from copy import deepcopy + +from django.contrib import admin +from django.utils.translation import ugettext_lazy as _ + +from cartridge.shop.models import Product, ProductImage, ProductVariation +from cartridge.shop.admin import ProductAdmin, ProductImageAdmin, ProductVariationAdmin + +from mezzanine.blog.admin import BlogPostAdmin +from mezzanine.blog.models import BlogPost + +# Register your models here. +from .models import Gallery +from .models import Grading +from .models import Link +from .models import Mix +from .models import News +from .models import Shop +from .models import Theme +from .models import User +from .models import Artist +from .models import Label +from .models import Country +from .models import Record +from .models import Podcast +from .models import ConditionGrading + +class RecordInline(admin.StackedInline): + model = Record + extra = 0 + max_num = 0 + +list_display = ProductAdmin.list_display +list_display.remove("sale_price") +list_editable = ProductAdmin.list_editable +list_editable.remove("sale_price") + +class RecordProductAdmin(ProductAdmin): + inlines = [ProductImageAdmin, ProductVariationAdmin, RecordInline] + +class RecordAdmin(admin.ModelAdmin): + search_fields = ["title", "artist__name", "label__name"] + filter_horizontal = ["performers"] + + +sale_fields = ("sale_price", "sale_from", "sale_to") +variation_fields = ProductVariationAdmin.fields +for field in sale_fields: + variation_fields.remove(field) + + +class myProductVariationAdmin(ProductVariationAdmin): + fields = variation_fields + +blog_fieldsets = deepcopy(BlogPostAdmin.fieldsets) +blog_fieldsets.insert(1, (_("Related products"), { + "classes": ("collapse-closed",), + "fields": ("related_products",)})) +blog_filter_horizontal = BlogPostAdmin.filter_horizontal +blog_filter_horizontal += ("related_products", ) + + +class MyBlogPostAdmin(BlogPostAdmin): + fieldsets = blog_fieldsets + filter_horizontal = blog_filter_horizontal + +admin.site.unregister(BlogPost) +admin.site.register(BlogPost, MyBlogPostAdmin) + + +admin.site.unregister(Product) +admin.site.register(Product, RecordProductAdmin) + +admin.site.register(Gallery) +admin.site.register(Grading) +admin.site.register(Link) +admin.site.register(Mix) +admin.site.register(News) +admin.site.register(Shop) +admin.site.register(Theme) +admin.site.register(User) +admin.site.register(Artist) +admin.site.register(Label) +admin.site.register(Country) +admin.site.register(Record, RecordAdmin) +admin.site.register(Podcast) +admin.site.register(ConditionGrading) diff --git a/app/records/management/__init__.py b/app/records/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/records/management/commands/__init__.py b/app/records/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/records/management/commands/populate_db.py b/app/records/management/commands/populate_db.py new file mode 100644 index 0000000..f063718 --- /dev/null +++ b/app/records/management/commands/populate_db.py @@ -0,0 +1,631 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from django.core.management.base import BaseCommand, CommandError + +# MYSQL command to restore the old SQL DB +# chown -R mysql:mysql /var/lib/mysql/ +# mysqld_safe & +# mysql -u digger -p diggersdigest < /var/lib/mysql/diggersdigest.sql + + +import os +from records import models as rec_models +from cartridge.shop.models import Product, ProductVariation, Category, ProductImage +import datetime +from diggersdigest import settings + + +import HTMLParser +parser = HTMLParser.HTMLParser() +import string + +import mezzanine.blog.models as blog_models +from mezzanine.pages.models import Page + + +# Clean Up DB +try: + obj = rec_models.Shop.objects.get(titre='nkvbhbjh') + obj.delete() +except rec_models.Shop.DoesNotExist: + pass + +DOLLAR_TO_EURO = 0.898642152 # 04/09/2015 + +RECORDS_TO_SKIP = [2734, 1006, 1393] # Doublons +RECORDS_TO_SKIP.append(3323) # old Fake records = "We buy records" page + +OLD_SHOP_LIST = rec_models.Shop.objects.exclude(pk__in = RECORDS_TO_SKIP) + + +# Check mp3 in media upload path +def check_mp3(file_list, file_path): + abs_file_path = os.path.join(settings.MEDIA_ROOT, file_path) + file_exists = [os.path.exists(os.path.join(abs_file_path, file_name)) + for file_name in file_list] + + missing_files = [file_name for (file_name, ok) in zip(file_list, file_exists) if not ok] + if missing_files: + print "Missing files in path : %s\n" % file_path + print '\n'.join(missing_files) + else: + print "No missing MP3 Mix files" + return missing_files + +# Get year and decade for record metadata +def get_year(date): + if date == '197': + date = date + '?' + try: + year = int(date) + decade = (year // 10 ) * 10 + except ValueError: + if (date == '196?') | (string.find(date, '60')>-1): + year = None + decade = '1960' + elif (date == '197?') | (date=='197.') | (string.find(date, '70')>-1): + year = None + decade = '1970' + elif (date == '198?') | (date == 'late 198') | (string.find(date, '80')>-1): + year = None + decade = 1980 + elif (date == '199?') | (string.find(date, '90')>-1): + year = None + decade = 1990 + else: + year, decade = None, None + return (year, decade) + + +class Command(BaseCommand): + help = 'Populate the new database from the original backup' + + ## def add_arguments(self, parser): + ## parser.add_argument('poll_id', nargs='+', type=int) + + # Label + def populate_label(self): + old_labels = set([shop.label for shop in OLD_SHOP_LIST]) + count = 0 + for lab in old_labels: + obj, created = rec_models.Label.objects.get_or_create(name = lab) + if created: + count += 1 + + self.stdout.write('Labels\n------\n') + self.stdout.write("\t%d new label objects" % count) + self.stdout.write("\t%d labels in DB ( %d in old DB)" % (len(rec_models.Label.objects.all()), len(old_labels))) + + # Artist + def populate_artist(self): + Artist = rec_models.Artist + old_artist = set([shop.artiste for shop in OLD_SHOP_LIST]) + count = 0 + for artist in old_artist: + obj, created = Artist.objects.get_or_create(name = artist) + if created: + count += 1 + self.stdout.write('Artist\n------\n') + self.stdout.write("\t%d new artist objects" % count) + self.stdout.write("\t%d artists in DB ( %d in old DB)" % (len(Artist.objects.all()), len(old_artist))) + + # Country + def populate_country(self): + Country = rec_models.Country + old_country = set([shop.pays for shop in OLD_SHOP_LIST]) + count = 0 + for country in old_country: + obj, created = Country.objects.get_or_create(name = country) + if created: + count += 1 + + self.stdout.write('Country\n------\n') + self.stdout.write("\t%d new country objects" % count) + self.stdout.write("\t%d countries in DB ( %d in old DB)" % (len(Country.objects.all()), len(old_country))) + + + + + # PODCAST FROM MIX + def populate_podcast(self, user): + MIX_CATEGORY = 'Podcast' + blog_models.BlogCategory.objects.get_or_create(title=MIX_CATEGORY) + + PODCAST_IMG_PATH = os.path.join('uploads/blog/mixes') + PODCAST_AUDIO_PATH = os.path.join('uploads/audio/mixes') + + self.stdout.write('Podcast\n-------\n') + + # MP3 for mix + mp3_mix_list = [mix.mp3 for mix in rec_models.Mix.objects.all()] + + check_mp3(mp3_mix_list, os.path.join(PODCAST_AUDIO_PATH)) + + ordered_mix_list = sorted(rec_models.Mix.objects.all(), key=lambda mix: mix.ordre) + count = 0 + for mix in ordered_mix_list: + if mix.visu1: + img_file = str(mix.id) + '.jpg' + img_path = os.path.join(PODCAST_IMG_PATH, img_file) + else: + img_path = '' + + audio_path = os.path.join(PODCAST_AUDIO_PATH, mix.mp3) + #status=mix.published+1, + #featured_image=img_path, + #user=digger) + if mix.titre: + obj, created = rec_models.Podcast.objects.get_or_create(pk=mix.pk, + title=parser.unescape(mix.titre), + user=user) + if created: + count += 1 + obj.audio=audio_path + obj.genre=mix.genre + obj.old_date=mix.date + desc = parser.unescape(mix.desc) + obj.description=desc + obj.content=desc # duplicate description in content + obj.status=mix.published+1 + obj.featured_image=img_path + obj.save() + + self.stdout.write("\t%d new Podcast objects" % count) + self.stdout.write("\t%d Podcast in DB ( %d in old DB)" % (len(rec_models.Podcast.objects.all()), len(ordered_mix_list))) + + ## audio = FileField(verbose_name=_("Audio File"), max_length=200, format="audio", + ## upload_to=upload_to("records.Podcast.audio", "audio/mixes")) + ## genre = models.CharField(max_length=128) + ## # titre --> title + ## date = models.CharField(max_length=64) + ## # desc --> description + ## # mp3 --> audio + ## #visu1 = models.IntegerField() si 1 --> featured_image + ## # ordre : on laisse tombé ? + ## # published --> status / 0 --> CONTENT_STATUS_DRAFT = 1 / 1 CONTENT_STATUS_PUBLISHED = 2 + + ## def populate_pages(self): + ## obj, created = Page.objects.get_or_create(status=2, + ## #"_order"= 3, + ## title="Contact", + ## titles= "Contact", + ## content_model="form", + ## in_menus=[1, 2, 3], + ## slug="contact", + ## description= "Fill in the form below to get in touch with us." + ## ) + ## #obj.delete() + ## #obj.description= "Fill in the form below to get in touch with us." + + # NEWS + def populate_news(self): + ## news fields : + ## 'texte' --> content + ## 'nomlien' + ## 'ordre' --> _order, doublons à gérer + ## 'id' --> pk + ## 'titre' --> title + ## 'typelien', + ## 'position', + ## 'adresslien', + ## 'visu1', + ## 'published'] + self.stdout.write('News\n-------\n') + + news_date = { + 3: datetime.datetime(2007, 11, 8, 19, 21, 39), + 5: datetime.datetime(2007, 12, 31, 0, 19, 13), + 6: datetime.datetime(2008, 1, 17, 12, 44, 50), + 7: datetime.datetime(2008, 2, 13, 13, 21, 19), + 12: datetime.datetime(2008, 4, 16, 0, 14, 23), + 13: datetime.datetime(2013, 6, 26, 1, 55, 4, 1817), + 14: datetime.datetime(2008, 6, 12, 21, 43, 46), + 16: datetime.datetime(2008, 6, 17, 17, 23, 36), + 18: datetime.datetime(2008, 7, 15, 23, 7, 42), + 19: datetime.datetime(2008, 8, 21, 15, 44, 30), + 20: datetime.datetime(2013, 3, 8, 17, 51, 12, 601338), + 28: datetime.datetime(2009, 2, 14, 0, 8, 36), + 33: datetime.datetime(2009, 3, 26, 22, 21, 2), + 35: datetime.datetime(2013, 11, 2, 22, 4, 45, 618265), + 40: datetime.datetime(2010, 7, 6, 1, 44, 38, 980145), + 41: datetime.datetime(2009, 7, 28, 13, 7, 52), + 42: datetime.datetime(2009, 7, 28, 1, 58, 32), + 43: datetime.datetime(2009, 8, 30, 13, 9, 53), + 46: datetime.datetime(2009, 10, 12, 16, 35, 14), + 50: datetime.datetime(2009, 11, 26, 14, 38, 49, 257703), + 54: datetime.datetime(2010, 1, 7, 18, 3, 1, 864172), + 56: datetime.datetime(2010, 2, 4, 16, 16, 32, 203171), + 59: datetime.datetime(2010, 2, 26, 3, 16, 22, 23171), + 61: datetime.datetime(2010, 3, 11, 1, 23, 19, 667171), + 63: datetime.datetime(2010, 3, 24, 23, 23, 55, 850107), + 65: datetime.datetime(2010, 6, 7, 22, 5, 40, 604645), + 67: datetime.datetime(2010, 6, 23, 11, 24, 18, 44646), + 70: datetime.datetime(2010, 9, 23, 8, 13, 3, 448061), + 71: datetime.datetime(2010, 10, 21, 12, 19, 52, 648562), + 72: datetime.datetime(2010, 11, 29, 10, 7, 31, 680399), + 73: datetime.datetime(2010, 12, 1, 11, 54, 59, 408399), + 75: datetime.datetime(2010, 12, 31, 12, 38, 46, 636398), + 77: datetime.datetime(2011, 5, 9, 0, 58, 42, 968596), + 78: datetime.datetime(2011, 1, 25, 17, 6, 58, 312900), + 79: datetime.datetime(2011, 2, 6, 14, 23, 57, 556399), + 80: datetime.datetime(2011, 3, 3, 1, 36, 13, 28095), + 81: datetime.datetime(2011, 3, 3, 1, 38, 52, 756095), + 82: datetime.datetime(2011, 4, 1, 22, 40, 39, 912095), + 83: datetime.datetime(2011, 3, 25, 1, 17, 34, 408096), + 84: datetime.datetime(2011, 4, 22, 18, 24, 10, 581095), + 85: datetime.datetime(2011, 5, 16, 20, 46, 34, 176095), + 86: datetime.datetime(2011, 6, 6, 21, 26, 10, 496720), + 87: datetime.datetime(2011, 6, 17, 20, 8, 51, 122313), + 88: datetime.datetime(2011, 7, 7, 21, 4, 15, 230314), + 90: datetime.datetime(2011, 8, 19, 21, 46, 36, 158814), + 92: datetime.datetime(2011, 9, 18, 23, 27, 12, 858075), + 94: datetime.datetime(2011, 11, 9, 15, 16, 30, 512660), + 96: datetime.datetime(2011, 12, 10, 20, 25, 45, 36660), + 97: datetime.datetime(2012, 1, 7, 23, 9, 25, 836659), + 98: datetime.datetime(2012, 1, 16, 7, 41, 1, 792660), + 100: datetime.datetime(2012, 1, 26, 15, 6, 12, 256660), + 101: datetime.datetime(2012, 2, 6, 13, 18, 23, 84660), + 102: datetime.datetime(2012, 2, 18, 12, 31, 46, 536660), + 103: datetime.datetime(2012, 3, 1, 13, 29, 11, 816662), + 106: datetime.datetime(2012, 4, 2, 19, 35, 33, 540661), + 107: datetime.datetime(2012, 4, 6, 20, 6, 14, 972661), + 108: datetime.datetime(2012, 4, 18, 21, 6, 9, 708893), + 109: datetime.datetime(2012, 5, 2, 23, 39, 54, 489173), + 110: datetime.datetime(2012, 5, 12, 3, 29, 56, 220660), + 111: datetime.datetime(2012, 5, 22, 13, 57, 14, 244660), + 112: datetime.datetime(2013, 2, 28, 14, 45, 6, 540838), + 113: datetime.datetime(2012, 6, 1, 1, 53, 31, 32661), + 114: datetime.datetime(2012, 6, 14, 20, 10, 54, 108660), + 115: datetime.datetime(2012, 7, 2, 14, 26, 30, 896721), + 116: datetime.datetime(2012, 7, 18, 16, 35, 42, 440661), + 117: datetime.datetime(2012, 8, 8, 10, 24, 52, 653338), + 119: datetime.datetime(2012, 9, 12, 1, 23, 55, 604838), + 120: datetime.datetime(2012, 10, 2, 0, 35, 16, 192839), + 121: datetime.datetime(2012, 10, 23, 2, 2, 26, 948838), + 122: datetime.datetime(2012, 11, 3, 15, 44, 35, 205838), + 123: datetime.datetime(2012, 11, 16, 2, 1, 20, 904838), + 124: datetime.datetime(2012, 11, 29, 11, 0, 21, 732838), + 125: datetime.datetime(2012, 12, 15, 23, 32, 42, 336838), + 126: datetime.datetime(2013, 1, 6, 2, 3, 38, 88839), + 127: datetime.datetime(2013, 1, 15, 18, 43, 30, 624838), + 128: datetime.datetime(2013, 1, 22, 12, 46, 44, 540839), + 129: datetime.datetime(2013, 1, 29, 19, 24, 57, 692838), + 130: datetime.datetime(2013, 2, 21, 23, 39, 9, 552838), + 131: datetime.datetime(2013, 3, 7, 0, 42, 0, 20838), + 132: datetime.datetime(2013, 3, 12, 11, 36, 25, 840838), + 133: datetime.datetime(2013, 3, 27, 10, 46, 48, 676838), + 134: datetime.datetime(2013, 4, 11, 5, 42, 2, 812839), + 135: datetime.datetime(2013, 4, 21, 10, 42, 31, 628838), + 136: datetime.datetime(2013, 5, 15, 23, 15, 54, 164838), + 137: datetime.datetime(2013, 6, 4, 15, 34, 36, 68817), + 138: datetime.datetime(2013, 6, 21, 14, 1, 29, 596317), + 139: datetime.datetime(2013, 7, 3, 1, 6, 11, 880319), + 140: datetime.datetime(2013, 7, 23, 15, 38, 2, 64317), + 141: datetime.datetime(2013, 8, 27, 3, 41, 39, 824317), + 142: datetime.datetime(2013, 9, 3, 7, 6, 29, 456317), + 143: datetime.datetime(2013, 9, 26, 13, 48, 21, 418265), + 144: datetime.datetime(2013, 10, 4, 14, 4, 13, 950265), + 147: datetime.datetime(2013, 11, 5, 1, 46, 33, 818265), + 148: datetime.datetime(2014, 1, 23, 8, 57, 4, 434265), + 149: datetime.datetime(2013, 11, 28, 15, 40, 41, 6265), + 150: datetime.datetime(2014, 1, 9, 12, 42, 44, 706265), + 151: datetime.datetime(2014, 1, 23, 8, 46, 25, 882265), + 152: datetime.datetime(2014, 2, 7, 23, 20, 0, 882265), + 153: datetime.datetime(2014, 2, 27, 20, 17, 38, 938266), + 154: datetime.datetime(2014, 3, 26, 9, 19, 12, 450265), + 155: datetime.datetime(2014, 4, 21, 18, 46, 11, 314265), + 156: datetime.datetime(2014, 5, 1, 18, 26, 7, 50265), + 158: datetime.datetime(2014, 5, 26, 11, 42, 36, 783265), + 159: datetime.datetime(2014, 6, 13, 21, 48, 29, 214265), + 160: datetime.datetime(2014, 6, 30, 21, 20, 7, 518265), + 161: datetime.datetime(2014, 7, 14, 19, 24, 40, 278265), + 162: datetime.datetime(2014, 8, 20, 2, 38, 41, 39946), + 163: datetime.datetime(2014, 9, 5, 19, 24, 46, 661639), + 164: datetime.datetime(2014, 9, 15, 22, 50, 48, 370368), + 165: datetime.datetime(2014, 10, 1, 12, 48, 31, 897806), + 166: datetime.datetime(2014, 10, 16, 16, 50, 59, 149418), + 167: datetime.datetime(2014, 11, 8, 20, 36, 14, 584649), + 168: datetime.datetime(2014, 11, 18, 2, 40, 48, 864752), + 169: datetime.datetime(2014, 12, 3, 11, 45, 31, 643276), + 170: datetime.datetime(2014, 12, 20, 0, 8, 48, 732320), + 171: datetime.datetime(2015, 1, 11, 11, 34, 36, 966855), + 172: datetime.datetime(2015, 1, 23, 1, 22, 51, 954981), + 173: datetime.datetime(2015, 2, 10, 9, 6, 39, 92265), + 174: datetime.datetime(2015, 2, 22, 3, 50, 50, 127304), + 175: datetime.datetime(2015, 3, 11, 12, 52, 56, 176986), + 176: datetime.datetime(2015, 4, 22, 16, 10, 44, 140259), + 177: datetime.datetime(2015, 4, 22, 16, 22, 47, 672255), + 178: datetime.datetime(2015, 5, 22, 10, 18, 45, 146728), + 179: datetime.datetime(2015, 6, 26, 1, 11, 8, 869148), + 180: datetime.datetime(2015, 7, 7, 23, 30, 46, 789683) + } + + ordered_news_list = sorted(rec_models.News.objects.all(), key=lambda news: news.ordre) + blog_models.BlogCategory.objects.get_or_create(title='News') + import parsedatetime + cal = parsedatetime.Calendar() + for n in ordered_news_list: + if n.position==1: + try: + d = cal.parseDateText(n.titre) + except AttributeError: + d = '' + print n.titre, ' - ', d + + + # Theme -> Category + def populate_category(self): + # This has to be run once as category are mezzanine Pages + # they would then be handle by a fixture for 'mezzanine pages' app + + + + shop_cat, created = Category.objects.get_or_create(title="Shop") + unclassified_cat, created = Category.objects.get_or_create( + title="Unclassified", + content="Unclassified records", + parent = shop_cat, + in_menus = [1,2]) + showcase, created = Category.objects.get_or_create( + title="Showcase", + content="Category for product to be placed on the HomePage", + parent = shop_cat, + in_menus = []) + + + theme_list = [theme for theme in rec_models.Theme.objects.all() + if (theme.published==1) and not(theme.id==39)] + count = 0 + for theme in theme_list: + category, create = Category.objects.get_or_create(pk=theme.id) + category.title = theme.nom + category.parent = shop_cat + category.in_menus = [1, 2] # Show only in header and left panel + category.save() + if create: + count +=1 + + self.stdout.write('Category\n-------\n') + self.stdout.write("\t%d new Category objects" % count) + + + # SHOPS TO RECORDS + def populate_record(self): + self.stdout.write('Records\n-------\n') + + AUDIO_PATH = 'uploads/audio/' + RECORDS_PATH = os.path.join(AUDIO_PATH, "records") + IMG_PATH = os.path.join('uploads/product') + + unclassified_cat, created = Category.objects.get(title="Unclassified") + UNCLASS_ID = unclassified_cat.pk + + THEME_TO_CATEGORY = { + # Published + 1: 1, # Jazz + 2: 2, # Prog / Psych / Rock + 37: 37, # Antilles / West Indies + 6: 6, # Experimental / Avant + 7: 7, # Soundtracks + 10: 10, # Electronic / Cosmic + 13: 13, # Middle East & Oriental + 15: 15, # Afro / Latin + 19: 19, # Library / Euro Grooves + 29: 29, # Prog / Psych / Pop 7 + 22: 22, # Jazz Funk / Fusion + 24: 24, # French Sounds + 28: 28, # Breaks & Samples + 30: 30, # Selected Reissues + 33: 33, # Soul / Funk + 38: 38, # OUR PRODUCTION + # Unpublished categories classified above + 42: 37, # SOLD OUT ANTILLES WEST INDIES + 16: 19, # SOLD OUT LIBRARY 0 + 41:1, # SOLD OUT JAZZ 0 + 25: 10, # SOLD OUT ELECTRO COSMIC + # New category for old sold-out and unclass records + UNCLASS_ID: UNCLASS_ID, + # Unpublished and unclassified categories + 17: UNCLASS_ID, # IN STOCK ->> UNCLASSIFIED + 18: UNCLASS_ID, # SOLD OUT ->> UNCLASSIFIED + 9: UNCLASS_ID, # Stuffs->> UNCLASSIFIED + 27: UNCLASS_ID, # SOLD OUT DEPOT->> UNCLASSIFIED + 40: UNCLASS_ID, # Brazilian Music->> UNCLASSIFIED + 31: UNCLASS_ID, # NEW ! 20 € & UNDER->> UNCLASSIFIED + # Fake records category --> turn into a Page in fixture + # 39 WE BUY RECORDS + } + + for shop in OLD_SHOP_LIST: + + # Get or Create record + record, created = rec_models.Record.objects.get_or_create(pk=shop.id) + # Set record metadata + record.title = parser.unescape(shop.titre) + record.country = rec_models.Country.objects.get(name = shop.pays) + record.audio_file = os.path.join(RECORDS_PATH, shop.mp3) + # product_id --> FK + + record.release_year , record.release_decade = get_year(shop.date) + record.date_text = shop.date # To be deleted later ? + record.record_status = shop.new + record.cover_condition_id = shop.cover + record.vinyl_condition_id = shop.vinyl + record.label = rec_models.Label.objects.get(name = shop.label) + record.artist = rec_models.Artist.objects.get(name = shop.artiste) + record.save() + + self.stdout.write('------') + self.stdout.write('Record %d - %d' % (record.pk,len(OLD_SHOP_LIST))) + + # -------------------------- + # Create associated Product + # -------------------------- + if isinstance(record.product, Product): + product = record.product + else: + product = Product.objects.create() + # Fill records ForeignKey to Product + record.product = product + record.save() + + + product.variations.manage_empty() + + # Metadata + product.title = record.title + product.content = shop.desc + + # Category + theme = rec_models.Theme.objects.get(id=shop.theme) + category_id = THEME_TO_CATEGORY[theme.pk] + category = Category.objects.get(pk=category_id) + product.categories.add(category) + + # Manage price / availability + product.available = (theme.published == 1) & (shop.published==1) + product.num_in_stock = int(product.available) + + if shop.devise == 3: #Euros + product.unit_price = shop.prix + elif shop.devise == 1: # Dollar + product.price = round(shop.prix * DOLLAR_TO_EURO) + product.sale_price = None # No sale / pas de soldes + + product.save() # Needed to copy the price fields to the default variation + + # Cover image + img_file = os.path.join(IMG_PATH, str(shop.pk)+'.jpg') + abs_img_file = os.path.join(settings.MEDIA_ROOT, img_file) + if os.path.exists(abs_img_file): + image, created = ProductImage.objects.get_or_create( + file=img_file, + product=product) + product.variations.set_default_images([]) + + product.copy_default_variation() + product.save() + + default_variation = product.variations.get(default=True) + default_variation.sku = "DD_REC_" + str(record.pk) + default_variation.save() + + self.stdout.write('Product %d / %d --> %s' % (product.pk,len(OLD_SHOP_LIST), product.available)) + + #self.stdout.write('\b\b\b\b%.4d' % record.pk) + #print record.__dict__ + + ## class Shop(models.Model): + ## theme = models.IntegerField() + ## artiste = models.CharField(max_length=128) + ## new = models.IntegerField() + ## titre = models.CharField(max_length=128) + ## label = models.CharField(max_length=128) + ## date = models.CharField(max_length=8) + ## pays = models.CharField(max_length=128) + ## desc = models.TextField() + ## cover = models.IntegerField() + ## vinyl = models.IntegerField() + ## prix = models.IntegerField() + ## devise = models.IntegerField() + ## mp3 = models.CharField(max_length=128) + ## visu1 = models.IntegerField() + ## ordre = models.IntegerField() + ## published = models.IntegerField() + + ## class Record(Product): + ## """ + ## Model for Record + ## """ + + + def handle(self, *args, **options): + ## for poll_id in options['poll_id']: + ## try: + ## poll = Poll.objects.get(pk=poll_id) + ## except Poll.DoesNotExist: + ## raise CommandError('Poll "%s" does not exist' % poll_id) + + ## poll.opened = False + ## poll.save() + + ## self.stdout.write('Successfully closed poll "%s"' % poll_id) + from django.contrib.auth import models as auth_models + digger, created = auth_models.User.objects.get_or_create(username=u'digger', + password=u'admin', + is_staff=True, + email=u'') + + self.populate_category() + self.populate_label() + self.populate_artist() + self.populate_country() + #self.populate_podcast(user=digger) + #self.populate_pages() + #self.populate_news() + + self.populate_record() + + + +# MP3 +mp3_shop_list = [shop.mp3 for shop in OLD_SHOP_LIST] + + +AUDIO_PATH = os.path.join(settings.MEDIA_ROOT, 'uploads/audio/') +MIX_PATH = os.path.join(AUDIO_PATH, 'mixes') +RECORDS_PATH = os.path.join(AUDIO_PATH, "records") + + + + + +# check_mp3(mp3_shop_list, RECORDS_PATH) + + +# GRADING + +Grading_Dict = { + 'SS': { + 'name': "Still Sealed", + 'description': "in perfect condition, no wear"}, + 'M' : { + 'name': "Mint" , + 'description': "Still in new condition, no imperfections, a Perfect Copy !"}, + 'NM' : { + 'name': "Near mint" , + 'description': "Imperceptible wear, full vinyl gloss"}, + 'EX' : { + 'name': "Excellent", + 'description': "Only very slight wear and paper scuffs, vinyl still glossy with very rare noise"}, + 'VG++' : { + 'name': "Near Excellent", + 'description': "Light surface noise and wear but fairly minor, vinyl still plays nicely"}, + 'VG+' : { + 'name': "Very Very Good", + 'description': "Evident groove wear or minor scuff marks, surface noise noticeable but does not really affect the playing"}, + 'VG' : { + 'name': "Very Good", + 'description': "Wear and scuffs are more evident and many surface noises are noticeable"}, + 'VG-' : { + 'name': "Good to Very Good", + 'description': "Heavy groove wear or scuffing, plays noisily"} + } + + +count = 0 +for grade in rec_models.Grading.objects.all(): + name = Grading_Dict[grade.nom]['name'] + desc = Grading_Dict[grade.nom]['description'] + obj, created = rec_models.ConditionGrading.objects.get_or_create(id=grade.id, + abbr=grade.nom, + name=name, + description=desc) + + if created: + print "Create new GradingCondition : %s" % obj.name + count += 1 + + +print "%d fiches GradingCondition crées" % count diff --git a/app/records/migrations/0001_initial.py b/app/records/migrations/0001_initial.py new file mode 100644 index 0000000..d334ade --- /dev/null +++ b/app/records/migrations/0001_initial.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion +import mezzanine.core.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('blog', '0002_auto_20150527_1555'), + ] + + operations = [ + migrations.CreateModel( + name='Gallery', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('titre', models.CharField(max_length=124)), + ('visu1', models.IntegerField()), + ('visu2', models.IntegerField()), + ('visu3', models.IntegerField()), + ('visu4', models.IntegerField()), + ('visu5', models.IntegerField()), + ('visu6', models.IntegerField()), + ('visu7', models.IntegerField()), + ('visu8', models.IntegerField()), + ('visu9', models.IntegerField()), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_Gallery', + 'db_table': 'gallery', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Grading', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('nom', models.CharField(max_length=5)), + ], + options={ + 'verbose_name': 'OLD_Grading', + 'db_table': 'grading', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Link', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('section', models.IntegerField()), + ('adress', models.CharField(max_length=124)), + ('desc', models.TextField()), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_Link', + 'db_table': 'links', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Mix', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('genre', models.CharField(max_length=128)), + ('titre', models.CharField(max_length=128)), + ('date', models.CharField(max_length=64)), + ('desc', models.TextField()), + ('mp3', models.CharField(max_length=128)), + ('visu1', models.IntegerField()), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_Mix', + 'db_table': 'mix', + 'managed': False, + 'verbose_name_plural': 'OLD_Mixes', + }, + ), + migrations.CreateModel( + name='News', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('titre', models.CharField(max_length=128)), + ('texte', models.TextField()), + ('visu1', models.IntegerField()), + ('position', models.IntegerField()), + ('nomlien', models.CharField(max_length=128)), + ('adresslien', models.CharField(max_length=255)), + ('typelien', models.IntegerField()), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_News', + 'db_table': 'news', + 'managed': False, + 'verbose_name_plural': 'OLD_News', + }, + ), + migrations.CreateModel( + name='Shop', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('theme', models.IntegerField()), + ('artiste', models.CharField(max_length=128)), + ('new', models.IntegerField()), + ('titre', models.CharField(max_length=128)), + ('label', models.CharField(max_length=128)), + ('date', models.CharField(max_length=8)), + ('pays', models.CharField(max_length=128)), + ('desc', models.TextField()), + ('cover', models.IntegerField()), + ('vinyl', models.IntegerField()), + ('prix', models.IntegerField()), + ('devise', models.IntegerField()), + ('mp3', models.CharField(max_length=128)), + ('visu1', models.IntegerField()), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_Shop', + 'db_table': 'shop', + 'managed': False, + }, + ), + migrations.CreateModel( + name='Theme', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('nom', models.CharField(max_length=124)), + ('ordre', models.IntegerField()), + ('published', models.IntegerField()), + ], + options={ + 'verbose_name': 'OLD_Theme', + 'db_table': 'theme', + 'managed': False, + }, + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('username', models.CharField(max_length=10)), + ('password', models.CharField(max_length=10)), + ('type', models.IntegerField()), + ], + options={ + 'db_table': 'user', + 'managed': False, + 'verbose_name_plural': 'OLD_User', + }, + ), + migrations.CreateModel( + name='Artist', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128)), + ], + ), + migrations.CreateModel( + name='ConditionGrading', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('abbr', models.CharField(unique=True, max_length=5, verbose_name='abbreviation')), + ('name', models.CharField(max_length=128, verbose_name='name')), + ('description', models.TextField(verbose_name='description')), + ], + ), + migrations.CreateModel( + name='Country', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128)), + ], + ), + migrations.CreateModel( + name='Label', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('name', models.CharField(max_length=128)), + ], + ), + migrations.CreateModel( + name='Podcast', + fields=[ + ('blogpost_ptr', models.OneToOneField(parent_link=True, auto_created=True, primary_key=True, serialize=False, to='blog.BlogPost')), + ('audio', mezzanine.core.fields.FileField(max_length=200, verbose_name='Audio File')), + ('genre', models.CharField(max_length=128, null=True)), + ('old_date', models.CharField(max_length=64, null=True)), + ('mix_cloud_url', models.URLField(null=True)), + ], + options={ + 'abstract': False, + }, + bases=('blog.blogpost',), + ), + migrations.CreateModel( + name='Record', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.CharField(max_length=128)), + ('record_status', models.IntegerField(default=1, verbose_name='record status', choices=[(0, ''), (1, 'New'), (2, 'On Hold'), (3, 'Just Sold')])), + ('release_date', models.DateField(null=True, verbose_name='release date')), + ('audio_file', mezzanine.core.fields.FileField(max_length=1024, null=True, verbose_name='audio file')), + ('artist', models.ForeignKey(related_name='records_artists', on_delete=django.db.models.deletion.SET_NULL, verbose_name='artist', to='records.Artist', null=True)), + ('country', models.ForeignKey(related_name='records', on_delete=django.db.models.deletion.SET_NULL, verbose_name='country', to='records.Country', null=True)), + ('label', models.ForeignKey(related_name='records', on_delete=django.db.models.deletion.SET_NULL, verbose_name='label', to='records.Label', null=True)), + ('performers', models.ManyToManyField(related_name='records_performers', verbose_name='performer', to='records.Artist')), + ], + ), + ] diff --git a/app/records/migrations/0002_auto_20150831_1518.py b/app/records/migrations/0002_auto_20150831_1518.py new file mode 100644 index 0000000..206938f --- /dev/null +++ b/app/records/migrations/0002_auto_20150831_1518.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0001_initial'), + ('records', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='record', + name='product', + field=models.ForeignKey(related_name='records', on_delete=django.db.models.deletion.SET_NULL, verbose_name='product', to='shop.Product', null=True), + ), + migrations.AlterField( + model_name='record', + name='performers', + field=models.ManyToManyField(related_name='records_performers', verbose_name='performers', to='records.Artist'), + ), + ] diff --git a/app/records/migrations/0003_record_date_text.py b/app/records/migrations/0003_record_date_text.py new file mode 100644 index 0000000..a82ad71 --- /dev/null +++ b/app/records/migrations/0003_record_date_text.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0002_auto_20150831_1518'), + ] + + operations = [ + migrations.AddField( + model_name='record', + name='date_text', + field=models.CharField(max_length=8, null=True, verbose_name='date text'), + ), + ] diff --git a/app/records/migrations/0004_auto_20150904_0733.py b/app/records/migrations/0004_auto_20150904_0733.py new file mode 100644 index 0000000..8c22f3e --- /dev/null +++ b/app/records/migrations/0004_auto_20150904_0733.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0003_record_date_text'), + ] + + operations = [ + migrations.RemoveField( + model_name='record', + name='release_date', + ), + migrations.AddField( + model_name='record', + name='release_decade', + field=models.CharField(max_length=8, null=True, verbose_name='release decade', choices=[(1920, '1920s'), (1930, '1930s'), (1940, '1940s'), (1950, '1950s'), (1960, '1960s'), (1970, '1970s'), (1980, '1980s'), (1990, '1990s'), (2000, '2000s'), (2010, '2010s'), (2020, '2020s')]), + ), + migrations.AddField( + model_name='record', + name='release_year', + field=models.IntegerField(null=True, verbose_name='release year', choices=[(1920, 1920), (1921, 1921), (1922, 1922), (1923, 1923), (1924, 1924), (1925, 1925), (1926, 1926), (1927, 1927), (1928, 1928), (1929, 1929), (1930, 1930), (1931, 1931), (1932, 1932), (1933, 1933), (1934, 1934), (1935, 1935), (1936, 1936), (1937, 1937), (1938, 1938), (1939, 1939), (1940, 1940), (1941, 1941), (1942, 1942), (1943, 1943), (1944, 1944), (1945, 1945), (1946, 1946), (1947, 1947), (1948, 1948), (1949, 1949), (1950, 1950), (1951, 1951), (1952, 1952), (1953, 1953), (1954, 1954), (1955, 1955), (1956, 1956), (1957, 1957), (1958, 1958), (1959, 1959), (1960, 1960), (1961, 1961), (1962, 1962), (1963, 1963), (1964, 1964), (1965, 1965), (1966, 1966), (1967, 1967), (1968, 1968), (1969, 1969), (1970, 1970), (1971, 1971), (1972, 1972), (1973, 1973), (1974, 1974), (1975, 1975), (1976, 1976), (1977, 1977), (1978, 1978), (1979, 1979), (1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018), (2019, 2019), (2020, 2020), (2021, 2021), (2022, 2022), (2023, 2023), (2024, 2024)]), + ), + ] diff --git a/app/records/migrations/0005_auto_20150904_1309.py b/app/records/migrations/0005_auto_20150904_1309.py new file mode 100644 index 0000000..070bd6c --- /dev/null +++ b/app/records/migrations/0005_auto_20150904_1309.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0004_auto_20150904_0733'), + ] + + operations = [ + migrations.AlterField( + model_name='record', + name='release_decade', + field=models.IntegerField(null=True, verbose_name='release decade', choices=[(1920, '1920s'), (1930, '1930s'), (1940, '1940s'), (1950, '1950s'), (1960, '1960s'), (1970, '1970s'), (1980, '1980s'), (1990, '1990s'), (2000, '2000s'), (2010, '2010s'), (2020, '2020s')]), + ), + ] diff --git a/app/records/migrations/0006_auto_20150907_0835.py b/app/records/migrations/0006_auto_20150907_0835.py new file mode 100644 index 0000000..2b8b73a --- /dev/null +++ b/app/records/migrations/0006_auto_20150907_0835.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0005_auto_20150904_1309'), + ] + + operations = [ + migrations.AddField( + model_name='record', + name='cover_condition', + field=models.ForeignKey(related_name='records_cover_condition', on_delete=django.db.models.deletion.SET_NULL, verbose_name='cover condition', to='records.ConditionGrading', null=True), + ), + migrations.AddField( + model_name='record', + name='vinyl_condition', + field=models.ForeignKey(related_name='records_vinyl_condition', on_delete=django.db.models.deletion.SET_NULL, verbose_name='vinyl condition', to='records.ConditionGrading', null=True), + ), + ] diff --git a/app/records/migrations/0007_record_image.py b/app/records/migrations/0007_record_image.py new file mode 100644 index 0000000..9e4c70d --- /dev/null +++ b/app/records/migrations/0007_record_image.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('shop', '0003_auto_20150906_1911'), + ('records', '0006_auto_20150907_0835'), + ] + + operations = [ + migrations.AddField( + model_name='record', + name='image', + field=models.ForeignKey(on_delete=django.db.models.deletion.SET_NULL, verbose_name='Image', blank=True, to='shop.ProductImage', null=True), + ), + ] diff --git a/app/records/migrations/0008_auto_20150909_0818.py b/app/records/migrations/0008_auto_20150909_0818.py new file mode 100644 index 0000000..f74a927 --- /dev/null +++ b/app/records/migrations/0008_auto_20150909_0818.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0007_record_image'), + ] + + operations = [ + migrations.AlterField( + model_name='record', + name='performers', + field=models.ManyToManyField(related_name='records_performers', verbose_name='performers', to='records.Artist', blank=True), + ), + migrations.AlterField( + model_name='record', + name='product', + field=models.OneToOneField(related_name='record', null=True, on_delete=django.db.models.deletion.SET_NULL, verbose_name='product', to='shop.Product'), + ), + ] diff --git a/app/records/migrations/0009_remove_record_image.py b/app/records/migrations/0009_remove_record_image.py new file mode 100644 index 0000000..3e8458c --- /dev/null +++ b/app/records/migrations/0009_remove_record_image.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0008_auto_20150909_0818'), + ] + + operations = [ + migrations.RemoveField( + model_name='record', + name='image', + ), + ] diff --git a/app/records/migrations/0010_auto_20150909_1400.py b/app/records/migrations/0010_auto_20150909_1400.py new file mode 100644 index 0000000..acad3b9 --- /dev/null +++ b/app/records/migrations/0010_auto_20150909_1400.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('records', '0009_remove_record_image'), + ] + + operations = [ + migrations.AlterModelOptions( + name='artist', + options={'ordering': ['name']}, + ), + ] diff --git a/app/records/migrations/__init__.py b/app/records/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/records/models.py b/app/records/models.py new file mode 100644 index 0000000..277b13c --- /dev/null +++ b/app/records/models.py @@ -0,0 +1,346 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +import datetime +import os +import fnmatch +from importlib import import_module +from diggersdigest import settings +# from mezzanine.conf import settings + +from mezzanine.core.fields import FileField +from mezzanine.core.models import CONTENT_STATUS_DRAFT, CONTENT_STATUS_PUBLISHED +from mezzanine.blog.models import BlogPost +from mezzanine.utils.models import upload_to + +from cartridge.shop.models import Product, Category, Cart, Order, ProductVariation, DiscountCode +from paypal.standard.ipn.signals import payment_was_successful +from paypal.standard.models import ST_PP_COMPLETED +from paypal.standard.ipn.signals import valid_ipn_received + + +# Auto-generated Django models with manage.py inspectdb on the old database +# You'll have to do the following manually to clean this up: +# * Make sure each model has one field with primary_key=True +# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table +# Feel free to rename the models, but don't rename db_table values or field names. + +class Gallery(models.Model): + titre = models.CharField(max_length=124) + visu1 = models.IntegerField() + visu2 = models.IntegerField() + visu3 = models.IntegerField() + visu4 = models.IntegerField() + visu5 = models.IntegerField() + visu6 = models.IntegerField() + visu7 = models.IntegerField() + visu8 = models.IntegerField() + visu9 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_Gallery' + managed = False + db_table = 'gallery' + + def __unicode__(self): + return self.titre + + + +class Grading(models.Model): + nom = models.CharField(max_length=5) + + class Meta: + verbose_name = 'OLD_Grading' + managed = False + db_table = 'grading' + + def __unicode__(self): + return self.nom + + + +class Link(models.Model): + section = models.IntegerField() + adress = models.CharField(max_length=124) + desc = models.TextField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_Link' + managed = False + db_table = 'links' + + def __unicode__(self): + return self.adress + + +class Mix(models.Model): + genre = models.CharField(max_length=128) + titre = models.CharField(max_length=128) + date = models.CharField(max_length=64) + desc = models.TextField() + mp3 = models.CharField(max_length=128) + visu1 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_Mix' + verbose_name_plural = 'OLD_Mixes' + managed = False + db_table = 'mix' + + def __unicode__(self): + return self.titre + + +class News(models.Model): + titre = models.CharField(max_length=128) + texte = models.TextField() + visu1 = models.IntegerField() + position = models.IntegerField() + nomlien = models.CharField(max_length=128) + adresslien = models.CharField(max_length=255) + typelien = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_News' + verbose_name_plural = 'OLD_News' + managed = False + db_table = 'news' + + def __unicode__(self): + return self.titre + + +class Shop(models.Model): + theme = models.IntegerField() + artiste = models.CharField(max_length=128) + new = models.IntegerField() + titre = models.CharField(max_length=128) + label = models.CharField(max_length=128) + date = models.CharField(max_length=8) + pays = models.CharField(max_length=128) + desc = models.TextField() + cover = models.IntegerField() + vinyl = models.IntegerField() + prix = models.IntegerField() + devise = models.IntegerField() + mp3 = models.CharField(max_length=128) + visu1 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_Shop' + managed = False + db_table = 'shop' + + def __unicode__(self): + return self.titre + + +class Theme(models.Model): + nom = models.CharField(max_length=124) + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + verbose_name = 'OLD_Theme' + managed = False + db_table = 'theme' + + def __unicode__(self): + return self.nom + + +class User(models.Model): + username = models.CharField(max_length=10) + password = models.CharField(max_length=10) + type = models.IntegerField() + + class Meta: + verbose_name_plural = 'OLD_User' + managed = False + db_table = 'user' + + def __unicode__(self): + return self.username + + +# New models for the 'records' app + +class Artist(models.Model): + name = models.CharField(max_length=128) + def __unicode__(self): + return self.name + + class Meta: + ordering = ['name'] + +class Label(models.Model): + name = models.CharField(max_length=128) + def __unicode__(self): + return self.name + + class Meta: + ordering = ['name'] + +class Country(models.Model): + name = models.CharField(max_length=128) + def __unicode__(self): + return self.name + + class Meta: + ordering = ['name'] + + +class ConditionGrading(models.Model): + + abbr = models.CharField(_('abbreviation'), max_length=5, unique=True) + name = models.CharField(_('name'), max_length=128) + description = models.TextField(_('description')) + + def __unicode__(self): + return " = ".join([self.abbr, self.name]) + +class Record(models.Model): + """ + Model for Record + """ +## # herited fields (from Product): +## # title --> shop.titre +## # categories --> shop.theme +## # price --> shop.prix +## # status --> shop.published + NEW = 1 + ON_HOLD = 2 + JUST_SOLD = 3 + NOVELTY_CHOICES = ( + (0, ''), + (NEW, 'New'), + (ON_HOLD, 'On Hold'), + (JUST_SOLD, 'Just Sold') + ) + + YEAR_START = 1920 + + YEAR_STOP = datetime.datetime.now().year + 10 + DECADE_START = (YEAR_START // 10) * 10 + YEAR_CHOICES = [(y, y) for y in range(YEAR_START, YEAR_STOP)] + DECADE_CHOICES = [(d, str(d)+'s') for d in range (DECADE_START, YEAR_STOP, 10)] + + title = models.CharField(max_length=128) + artist = models.ForeignKey(Artist, verbose_name=_('artist'), related_name='records_artists', null=True, on_delete=models.SET_NULL) + performers = models.ManyToManyField(Artist, verbose_name=_('performers'), related_name='records_performers', blank=True) + record_status = models.IntegerField(_('record status'), choices=NOVELTY_CHOICES, default=NEW) + label = models.ForeignKey(Label, verbose_name=_('label'), related_name='records', null=True, on_delete=models.SET_NULL) + release_year = models.IntegerField(_('release year'), null=True, choices=YEAR_CHOICES) + release_decade = models.IntegerField(_('release decade'), null=True, choices=DECADE_CHOICES) + date_text = models.CharField(_('date text'), max_length=8, null=True) + country = models.ForeignKey(Country, verbose_name=_('country'), related_name='records', null=True, on_delete=models.SET_NULL) + cover_condition = models.ForeignKey(ConditionGrading, verbose_name=_('cover condition'), related_name='records_cover_condition', null=True, on_delete=models.SET_NULL) + vinyl_condition = models.ForeignKey(ConditionGrading, verbose_name=_('vinyl condition'), related_name='records_vinyl_condition', null=True, on_delete=models.SET_NULL) + audio_file = FileField(_("audio file"), max_length=1024, format="audio", + upload_to=upload_to("records.Record.audio", "audio/records"), null=True) + product = models.OneToOneField(Product, verbose_name=_('product'), related_name='record', null=True, on_delete=models.SET_NULL) + + def __unicode__(self): + return " - ".join([self.artist.name, self.title]) + + def get_candidate_images(self): + candidates = [] + img_path = os.path.join(settings.MEDIA_ROOT, 'backup_images') + patterns = [] + if self.artist: + patterns.append(self.artist.name.strip().lower().replace(' ','')) + if self.title: + patterns.append(self.title.strip().lower().replace(' ','')) + for root, dirnames, filenames in os.walk(img_path): + for pattern in patterns: + for filename in filenames: + if fnmatch.fnmatch(filename.lower(), '*' + pattern + '*.jpg'): + candidates.append(os.path.join(root, filename)) + return candidates + + +class Podcast(BlogPost): + audio = FileField(verbose_name=_("Audio File"), max_length=200, format="Audio", + upload_to=upload_to("records.Podcast.audio", "audio/mixes")) + genre = models.CharField(max_length=128, null=True) + # titre --> title + old_date = models.CharField(max_length=64, null=True) + mix_cloud_url = models.URLField(null=True) + # desc --> description + # mp3 --> audio + #visu1 = models.IntegerField() + # ordre : on laisse tombé ? + # published --> status / 0 --> CONTENT_STATUS_DRAFT = 1 / 1 CONTENT_STATUS_PUBLISHED = 2 + + +def payment_complete(sender, **kwargs): + """Performs the same logic as the code in + cartridge.shop.models.Order.complete(), but fetches the session, + order, and cart objects from storage, rather than relying on the + request object being passed in (which it isn't, since this is + triggered on PayPal IPN callback).""" + + ipn_obj = sender + + if ipn_obj.custom and ipn_obj.invoice and ipn_obj.payment_status == ST_PP_COMPLETED: + s_key, cart_pk = ipn_obj.custom.split(',') + SessionStore = import_module(settings.SESSION_ENGINE) \ + .SessionStore + session = SessionStore(s_key) + + try: + cart = Cart.objects.get(id=cart_pk) + try: + order = Order.objects.get( + transaction_id=ipn_obj.invoice) + for field in order.session_fields: + if field in session: + del session[field] + try: + del session["order"] + except KeyError: + pass + + # Since we're manually changing session data outside of + # a normal request, need to force the session object to + # save after modifying its data. + session.save() + + for item in cart: + try: + variation = ProductVariation.objects.get( + sku=item.sku) + except ProductVariation.DoesNotExist: + pass + else: + variation.update_stock(item.quantity * -1) + variation.product.actions.purchased() + + code = session.get('discount_code') + if code: + DiscountCode.objects.active().filter(code=code) \ + .update(uses_remaining=F('uses_remaining') - 1) + cart.delete() + except Order.DoesNotExist: + pass + except Cart.DoesNotExist: + pass + +payment_was_successful.connect(payment_complete) +valid_ipn_received.connect(payment_complete) diff --git a/app/records/models_db.py b/app/records/models_db.py new file mode 100644 index 0000000..84129b2 --- /dev/null +++ b/app/records/models_db.py @@ -0,0 +1,125 @@ +# This is an auto-generated Django model module. +# You'll have to do the following manually to clean this up: +# * Rearrange models' order +# * Make sure each model has one field with primary_key=True +# * Remove `managed = False` lines if you wish to allow Django to create, modify, and delete the table +# Feel free to rename the models, but don't rename db_table values or field names. +# +# Also note: You'll have to insert the output of 'django-admin sqlcustom [app_label]' +# into your database. +from __future__ import unicode_literals + +from django.db import models + + +class Gallery(models.Model): + titre = models.CharField(max_length=124) + visu1 = models.IntegerField() + visu2 = models.IntegerField() + visu3 = models.IntegerField() + visu4 = models.IntegerField() + visu5 = models.IntegerField() + visu6 = models.IntegerField() + visu7 = models.IntegerField() + visu8 = models.IntegerField() + visu9 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'gallery' + + +class Grading(models.Model): + nom = models.CharField(max_length=5) + + class Meta: + managed = False + db_table = 'grading' + + +class Links(models.Model): + section = models.IntegerField() + adress = models.CharField(max_length=124) + desc = models.TextField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'links' + + +class Mix(models.Model): + genre = models.CharField(max_length=128) + titre = models.CharField(max_length=128) + date = models.CharField(max_length=64) + desc = models.TextField() + mp3 = models.CharField(max_length=128) + visu1 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'mix' + + +class News(models.Model): + titre = models.CharField(max_length=128) + texte = models.TextField() + visu1 = models.IntegerField() + position = models.IntegerField() + nomlien = models.CharField(max_length=128) + adresslien = models.CharField(max_length=255) + typelien = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'news' + + +class Shop(models.Model): + theme = models.IntegerField() + artiste = models.CharField(max_length=128) + new = models.IntegerField() + titre = models.CharField(max_length=128) + label = models.CharField(max_length=128) + date = models.CharField(max_length=8) + pays = models.CharField(max_length=128) + desc = models.TextField() + cover = models.IntegerField() + vinyl = models.IntegerField() + prix = models.IntegerField() + devise = models.IntegerField() + mp3 = models.CharField(max_length=128) + visu1 = models.IntegerField() + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'shop' + + +class Theme(models.Model): + nom = models.CharField(max_length=124) + ordre = models.IntegerField() + published = models.IntegerField() + + class Meta: + managed = False + db_table = 'theme' + + +class User(models.Model): + username = models.CharField(max_length=10) + password = models.CharField(max_length=10) + type = models.IntegerField() + + class Meta: + managed = False + db_table = 'user' diff --git a/app/records/static/css/dig2.css b/app/records/static/css/dig2.css new file mode 100644 index 0000000..c59f60e --- /dev/null +++ b/app/records/static/css/dig2.css @@ -0,0 +1,63 @@ +.container{ + background:url("../img/dotted_background.png" + ) repeat; +} + +.slim_margins{ + padding-right:1px; + padding-left:1px; +} + +body { + padding-top: 182px; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 2px solid #000; + margin: 1em 0; + padding: 0; +} + +.bg-white { + background-color: white; + padding: 5px; +} + +.description { + background-color: white; + padding: 1em 1em 1em 2em; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); + border: 1px solid #dddddd; + border-radius: 4px; +} + +.navbar-default { + background: white; + margin-bottom: 30px; +} + +.price:only-child, .coming-soon { + margin-top: 3px; + display: block; + font-weight: bold; +} + +.navbar-collapse { + margin-top: 50px; +} + +#logo { + float: left; +} + +#user_panel_header { + float: right; + margin-top: -90px; +} + +.audio { + margin-top: 2em; +} diff --git a/app/records/static/img/diggersdigest_400.png b/app/records/static/img/diggersdigest_400.png new file mode 100644 index 0000000..6d29912 Binary files /dev/null and b/app/records/static/img/diggersdigest_400.png differ diff --git a/app/records/static/img/dotted_background.png b/app/records/static/img/dotted_background.png new file mode 100644 index 0000000..cabfa99 Binary files /dev/null and b/app/records/static/img/dotted_background.png differ diff --git a/app/records/static/img/favicon.ico b/app/records/static/img/favicon.ico new file mode 100644 index 0000000..550d600 Binary files /dev/null and b/app/records/static/img/favicon.ico differ diff --git a/app/records/templatetags/__init__.py b/app/records/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/records/templatetags/records_tags.py b/app/records/templatetags/records_tags.py new file mode 100644 index 0000000..86ed577 --- /dev/null +++ b/app/records/templatetags/records_tags.py @@ -0,0 +1,30 @@ +from django.db.models import Q + +from mezzanine import template +from cartridge.shop.models import Product, Category + +register = template.Library() + + +@register.as_tag +def shop_recent_products(limit=5, category=None): + """ + Put a list of recently published products into the template + context. A tag title or slug, category title or slug or author's + username can also be specified to filter the recent posts returned. + Usage:: + {% blog_recent_posts 5 as recent_posts %} + {% blog_recent_posts limit=5 tag="django" as recent_posts %} + {% blog_recent_posts limit=5 category="python" as recent_posts %} + """ + products = Product.objects.published().filter(available=True).order_by("-publish_date") + title_or_slug = lambda s: Q(title=s) | Q(slug=s) + + if category is not None: + try: + category = Category.objects.get(title_or_slug(category)) + products = products.filter(categories=category) + except Category.DoesNotExist: + return [] + + return list(products[:limit]) diff --git a/app/records/tests.py b/app/records/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/records/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/records/views.py b/app/records/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/records/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/app/templates/accounts/account_profile_update.html b/app/templates/accounts/account_profile_update.html new file mode 100644 index 0000000..9ea1087 --- /dev/null +++ b/app/templates/accounts/account_profile_update.html @@ -0,0 +1,7 @@ +{% extends "accounts/account_form.html" %} +{% load i18n %} + +{% block account_form_actions %} +{{ block.super }} +{% trans "View past orders" %} +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..2e0ecc1 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,180 @@ + + +{% load pages_tags mezzanine_tags i18n staticfiles %} + + + + + + +{% block meta_title %}{% endblock %}{% if settings.SITE_TITLE %} | {{ settings.SITE_TITLE }}{% endif %} + + +{% ifinstalled mezzanine.blog %} + + +{% endifinstalled %} + +{% compress css %} + + + +{% if LANGUAGE_BIDI %} + +{% endif %} +{% ifinstalled cartridge.shop %} + +{% if LANGUAGE_BIDI %} + +{% endif %} + +{% endifinstalled %} +{% block extra_css %}{% endblock %} +{% endcompress %} + +{% compress js %} + + + +{% block extra_js %}{% endblock %} +{% endcompress %} + + + + + +{% block extra_head %}{% endblock %} + + + + + +
+ +{% nevercache %} +{% if messages %} +
+{% for message in messages %} +
+ + {{ message }} +
+{% endfor %} +
+{% endif %} +{% endnevercache %} + +

{% block title %}{% endblock %}

+ +{% comment - Remove breadcrumbs %} + +{% endcomment %} + +
+ +
+
+ +{% comment - Remove left panel %} +
+ {% block left_panel %} +
{% page_menu "pages/menus/tree.html" %}
+ {% endblock %} +
+{% endcomment %} + +
+ {% block main %}{% endblock %} +
+ +{% comment %} +
+
+
+ {% block right_panel %} + {% ifinstalled mezzanine.twitter %} + {% include "twitter/tweets.html" %} + {% endifinstalled %} + {% endblock %} +
+
+
+{% endcomment %} + +
+
+ + + +{% include "includes/footer_scripts.html" %} + + + diff --git a/app/templates/blog/blog_post_detail.html b/app/templates/blog/blog_post_detail.html new file mode 100644 index 0000000..94374fb --- /dev/null +++ b/app/templates/blog/blog_post_detail.html @@ -0,0 +1,136 @@ +{% extends "blog/blog_post_list.html" %} +{% load mezzanine_tags comment_tags keyword_tags rating_tags i18n disqus_tags %} + +{% block meta_title %}{{ blog_post.meta_title }}{% endblock %} + +{% block meta_keywords %}{% metablock %} +{% keywords_for blog_post as tags %} +{% for tag in tags %}{% if not forloop.first %}, {% endif %}{{ tag }}{% endfor %} +{% endmetablock %}{% endblock %} + +{% block meta_description %}{% metablock %} +{{ blog_post.description }} +{% endmetablock %}{% endblock %} + +{% block title %} +{% editable blog_post.title %}{{ blog_post.title }}{% endeditable %} +{% endblock %} + +{% block breadcrumb_menu %} +{{ block.super }} +
  • {{ blog_post.title }}
  • +{% endblock %} + +{% block main %} + +{% block blog_post_detail_postedby %} +{% editable blog_post.publish_date %} +
    + {% trans "Posted by" %}: + {% with blog_post.user as author %} + {{ author.get_full_name|default:author.username }} + {% endwith %} + {% blocktrans with sometime=blog_post.publish_date|timesince %}{{ sometime }} ago{% endblocktrans %} +
    +{% endeditable %} +{% endblock %} +{% block blog_post_detail_commentlink %} +

    + {% if blog_post.allow_comments %} + {% if settings.COMMENTS_DISQUS_SHORTNAME %} + ({% spaceless %} + {% trans "Comments" %} + {% endspaceless %}) + {% else %}({% spaceless %} + {% blocktrans count comments_count=blog_post.comments_count %}{{ comments_count }} comment{% plural %}{{ comments_count }} comments{% endblocktrans %} + {% endspaceless %}) + {% endif %} + {% endif %} +

    +{% endblock %} + +{% block blog_post_detail_featured_image %} +{% if settings.BLOG_USE_FEATURED_IMAGE and blog_post.featured_image %} +

    +{% endif %} +{% endblock %} + +{% if settings.COMMENTS_DISQUS_SHORTNAME %} +{% include "generic/includes/disqus_counts.html" %} +{% endif %} + +{% block blog_post_detail_content %} +{% editable blog_post.content %} +{{ blog_post.content|richtext_filters|safe }} +{% endeditable %} +{% endblock %} + +{% block blog_post_detail_keywords %} +{% keywords_for blog_post as tags %} +{% if tags %} +{% spaceless %} + +{% endspaceless %} +{% endif %} +{% endblock %} + +

    +{% comment %} +{% block blog_post_detail_rating %} +
    +
    + {% rating_for blog_post %} +
    +
    +{% endblock %} +{% endcomment %} + +{% block blog_post_detail_sharebuttons %} +{% set_short_url_for blog_post %} +{% trans "Share on Twitter" %} +{% trans "Share on Facebook" %} +{% endblock %} + +{% block blog_post_previous_next %} + +{% endblock %} + +{% block blog_post_detail_related_posts %} +{% if related_posts %} + +{% endif %} +{% endblock %} + +{% block blog_post_detail_comments %} +{% if blog_post.allow_comments %}{% comments_for blog_post %}{% endif %} +{% endblock %} + +{% endblock %} diff --git a/app/templates/blog/blog_post_list.html b/app/templates/blog/blog_post_list.html new file mode 100644 index 0000000..21ddf51 --- /dev/null +++ b/app/templates/blog/blog_post_list.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} +{% load i18n mezzanine_tags blog_tags keyword_tags disqus_tags %} + +{% block meta_title %}{% if page %}{{ page.meta_title }}{% else %}{% trans "Blog" %}{% endif %}{% endblock %} + +{% block meta_keywords %}{% metablock %} +{% keywords_for page as keywords %} +{% for keyword in keywords %} + {% if not forloop.first %}, {% endif %} + {{ keyword }} +{% endfor %} +{% endmetablock %}{% endblock %} + +{% block meta_description %}{% metablock %} +{{ page.description }} +{% endmetablock %}{% endblock %} + +{% block title %} +{% if page %} +{% editable page.title %}{{ page.title }}{% endeditable %} +{% else %} +{% trans "Blog" %} +{% endif %} +{% endblock %} + +{% block breadcrumb_menu %} +{{ block.super }} +{% if tag or category or year or month or author %} +
  • {% spaceless %} +{% if tag %} + {% trans "Tag:" %} {{ tag }} +{% else %}{% if category %} + {% trans "Category:" %} {{ category }} +{% else %}{% if year or month %} + {% if month %}{{ month }}, {% endif %}{{ year }} +{% else %}{% if author %} + {% trans "Author:" %} {{ author.get_full_name|default:author.username }} +{% endif %}{% endif %}{% endif %}{% endif %} +{% endspaceless %} +
  • +{% endif %} +{% endblock %} + +{% block main %} + +{% if tag or category or year or month or author %} + {% block blog_post_list_filterinfo %} +

    + {% if tag %} + {% trans "Viewing posts tagged" %} {{ tag }} + {% else %}{% if category %} + {% trans "Viewing posts for the category" %} {{ category }} + {% else %}{% if year or month %} + {% trans "Viewing posts from" %} {% if month %}{{ month }}, {% endif %} + {{ year }} + {% else %}{% if author %} + {% trans "Viewing posts by" %} + {{ author.get_full_name|default:author.username }} + {% endif %}{% endif %}{% endif %}{% endif %} + {% endblock %} +

    +{% else %} + {% if page %} + {% block blog_post_list_pagecontent %} + {% if page.get_content_model.content %} + {% editable page.get_content_model.content %} + {{ page.get_content_model.content|richtext_filters|safe }} + {% endeditable %} + {% endif %} + {% endblock %} + {% endif %} +{% endif %} + +{% for blog_post in blog_posts.object_list %} +{% block blog_post_list_post_title %} +{% editable blog_post.title %} +

    + {{ blog_post.title }} +

    +{% endeditable %} +{% endblock %} +{% block blog_post_list_post_metainfo %} +{% editable blog_post.publish_date %} +
    + {% trans "Posted by" %}: + {% with blog_post.user as author %} + {{ author.get_full_name|default:author.username }} + {% endwith %} + {% with blog_post.categories.all as categories %} + {% if categories %} + {% trans "in" %} + {% for category in categories %} + {{ category }}{% if not forloop.last %}, {% endif %} + {% endfor %} + {% endif %} + {% endwith %} + {% blocktrans with sometime=blog_post.publish_date|timesince %}{{ sometime }} ago{% endblocktrans %} +
    +{% endeditable %} +{% endblock %} + +{% if settings.BLOG_USE_FEATURED_IMAGE and blog_post.featured_image %} +{% block blog_post_list_post_featured_image %} + + + +{% endblock %} +{% endif %} + +{% block blog_post_list_post_content %} +{% editable blog_post.content %} +{{ blog_post.description_from_content|safe }} +{% endeditable %} +{% endblock %} + +{% block blog_post_list_post_links %} +
    + {% keywords_for blog_post as tags %} + {% if tags %} + + {% endif %} +

    + {% trans "read more" %} + {% if blog_post.allow_comments %} + / + {% if settings.COMMENTS_DISQUS_SHORTNAME %} + + {% trans "Comments" %} + + {% else %} + + {% blocktrans count comments_count=blog_post.comments_count %}{{ comments_count }} comment{% plural %}{{ comments_count }} comments{% endblocktrans %} + + {% endif %} + {% endif %} +

    +
    +{% endblock %} +{% endfor %} + +{% pagination_for blog_posts %} + +{% if settings.COMMENTS_DISQUS_SHORTNAME %} +{% include "generic/includes/disqus_counts.html" %} +{% endif %} + +{% endblock %} + +{% block right_panel %} +{% include "blog/includes/filter_panel.html" %} +{% endblock %} diff --git a/app/templates/blog/includes/filter_panel.html b/app/templates/blog/includes/filter_panel.html new file mode 100644 index 0000000..d2817d8 --- /dev/null +++ b/app/templates/blog/includes/filter_panel.html @@ -0,0 +1,101 @@ +{% load blog_tags keyword_tags mezzanine_tags i18n %} + +{% block blog_recent_posts %} +{% blog_recent_posts 5 as recent_posts %} +{% if recent_posts %} +

    {% trans "Recent Posts" %}

    + +{% endif %} +{% endblock %} + +{% block blog_months %} +{% blog_months as months %} +{% if months %} +

    {% trans "Archive" %}

    +{% for month in months %} + {% ifchanged month.date.year %} + {% if not forloop.first %}{% endif %} +
    {{ month.date.year }}
    +{% endif %} +{% endblock %} + +{% block blog_categories %} +{% blog_categories as categories %} +{% if categories %} +

    {% trans "Categories" %}

    + +{% endif %} +{% endblock %} + +{% block blog_keywords %} +{% keywords_for blog.blogpost as tags %} +{% if tags %} +

    {% trans "Tags" %}

    + +{% endif %} +{% endblock %} + +{% block blog_authors %} +{% blog_authors as authors %} +{% if authors %} +

    {% trans "Authors" %}

    + +{% endif %} +{% endblock %} + +{% block blog_feeds %} +

    {% trans "Feeds" %}

    +{% if tag %} + {% trans "RSS" %} / + {% trans "Atom" %} +{% endif %} +{% if category %} + {% trans "RSS" %} / + {% trans "Atom" %} +{% endif %} +{% if author %} + {% trans "RSS" %} / + {% trans "Atom" %} +{% endif %} +{% if not tag and not category and not author %} + {% trans "RSS" %} / + {% trans "Atom" %} +{% endif %} +{% endblock %} diff --git a/app/templates/email/base.html b/app/templates/email/base.html new file mode 100644 index 0000000..ec4ad78 --- /dev/null +++ b/app/templates/email/base.html @@ -0,0 +1,5 @@ +{% load i18n %} +{% get_current_language_bidi as LANGUAGE_BIDI %} + +{% block main %} +{% endblock %} diff --git a/app/templates/email/base.txt b/app/templates/email/base.txt new file mode 100644 index 0000000..a8416a3 --- /dev/null +++ b/app/templates/email/base.txt @@ -0,0 +1,3 @@ +{% block main %}{% endblock %} + +http://{{ request.get_host }} diff --git a/app/templates/email/order_receipt.html b/app/templates/email/order_receipt.html new file mode 100644 index 0000000..5a25212 --- /dev/null +++ b/app/templates/email/order_receipt.html @@ -0,0 +1,13 @@ +{% extends "email/base.html" %} +{% load i18n %} + + +{% block main %} + +{% if not LANGUAGE_BIDI %} + {% include "email/receipt.html" %} +{% else %} + {% include "email/receipt_rtl.html" %} +{% endif %} + +{% endblock %} diff --git a/app/templates/email/order_receipt.txt b/app/templates/email/order_receipt.txt new file mode 100644 index 0000000..f4e5e8b --- /dev/null +++ b/app/templates/email/order_receipt.txt @@ -0,0 +1,27 @@ +{% extends "email/base.txt" %} +{% load shop_tags i18n %} + +{% block main %} +{% trans "Dear" %} {{ order.billing_detail_first_name }}, + +{% trans "Your order has been successful, details are below." %} + +{% trans "Order ID:" %} #{{ order.id }} + +{% trans "Billing Details:" %} +{% for field, value in order_billing_detail_fields %} +{{ field }}: {{ value }} +{% endfor %} + +{% trans "Shipping Details:" %} +{% for field, value in order_shipping_detail_fields %} +{{ field }}: {{ value }} +{% endfor %} + +{% trans "Items Ordered:" %} +{% for item in order_items %} +{{ item.quantity }} x {{ item.description }} {{ item.unit_price|currency }} {% trans "each" %} +{% endfor %} + +{% order_totals_text %} +{% endblock %} diff --git a/app/templates/email/receipt.html b/app/templates/email/receipt.html new file mode 100644 index 0000000..97709a9 --- /dev/null +++ b/app/templates/email/receipt.html @@ -0,0 +1,6 @@ +{% load shop_tags i18n %} + +

    {% trans "Dear" %} {{ order.billing_detail_first_name }},

    +

    {% trans "Your order has been successful, details are below." %}

    +{% include "shop/includes/order_details.html" %} + diff --git a/app/templates/email/receipt_rtl.html b/app/templates/email/receipt_rtl.html new file mode 100644 index 0000000..19a007c --- /dev/null +++ b/app/templates/email/receipt_rtl.html @@ -0,0 +1,6 @@ +{% load shop_tags i18n %} + +

    ,{% trans "Dear" %} {{ order.billing_detail_first_name }}

    +

    {% trans "Your order has been successful, details are below." %}

    +{% include "shop/includes/order_details_rtl.html" %} + diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html new file mode 100644 index 0000000..65788de --- /dev/null +++ b/app/templates/errors/404.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block meta_title %} +{% trans "Page not found" %} +{% endblock %} + +{% block title %} +{% trans "Page not found" %} +{% endblock %} + +{% block main %} +
    +

    {% trans "Page not found" %}

    +
    {% trans "The page you requested does not exist." %}
    +
    +{% endblock %} diff --git a/app/templates/errors/500.html b/app/templates/errors/500.html new file mode 100644 index 0000000..26cbb4e --- /dev/null +++ b/app/templates/errors/500.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block meta_title %} +{% trans "Error" %} +{% endblock %} + +{% block title %} +{% trans "Error" %} +{% endblock %} + +{% block main %} +
    +

    {% trans "Error" %}

    +
    {% trans "Sorry, an error occurred." %}
    +
    +{% endblock %} diff --git a/app/templates/includes/editable_form.html b/app/templates/includes/editable_form.html new file mode 100644 index 0000000..d4ac039 --- /dev/null +++ b/app/templates/includes/editable_form.html @@ -0,0 +1,28 @@ +{% load i18n %} + +{# Edit form #} +