class Screenshot(models.Model): """ プロダクトのスクリーンショットモデル プロダクト管理者は何枚でもプロダクトに関連付けることが出来る """ def _get_upload_path(self, filename): basedir = os.path.join('products', self.product.slug, 'screenshots') return os.path.join(basedir, filename) image = ThumbnailField(_('Image'), upload_to=_get_upload_path, patterns=SCREENSHOT_IMAGE_SIZE_PATTERNS) product = UnsavedForeignKey(Product, verbose_name=_('Product'), editable=False, related_name='screenshots') class Meta: ordering = ('pk', ) verbose_name = _('Screen shot') verbose_name_plural = _('Screen shots') def __str__(self): return '{}({})'.format(self.image.name, self.product.title)
class Entry(models.Model): title = models.SlugField('title', unique=True) body = models.TextField('body') # # This is a usage of ThumbnailField. # You have to set ``patterns`` to generate thumbnails # thumbnail = ThumbnailField( 'thumbnail', upload_to='img/thumbnails', null=True, blank=True, pil_save_options={ # Options of PIL Image.save() method. # e.g. quality control 'quality': 5, }, patterns={ # Pattern Format: # <Name>: ( # (<square_size>,), # with defautl process_method # (<width>, <height>,), # with default process_method # (<width>, <height>, <method or method_name>), # (<width>, <height>, <method or method_name>, <method options>), # ) # # If Name is ``None`` that mean original image will be processed # with the pattern # # Convert original image to sepia and resize it to 800x400 ( # original size is 804x762) None: ((None, None, 'sepia'), (800, 400, 'resize')), # Create 640x480 resized thumbnail as large. 'large': ((640, 480, 'resize'),), # Create 320x240 cropped thumbnail as small. You can write short # pattern if the number of appling pattern is 1 'small': (320, 240, 'crop', {'left': 0, 'upper': 0}), # Create 160x120 thumbnail as tiny (use default process_method to # generate) 'tiny': (160, 120), # # These thumbnails are not generated while accessed. These can be # accessed with the follwoing code:: # # entry.thumbnail.large # entry.thumbnail.small # entry.thumbnail.tiny # # shortcut properties # entry.thumbnail.large_file # as entry.thumbnail.large.file # entry.thumbnail.large_path # as entry.thumbnail.large.path # entry.thumbnail.large_url # as entry.thumbnail.large.url # entry.thumbnail.large.size # as entry.thumbnail.large.size # 'nothing': None, }) class Meta: app_label = 'thumbnailfield'
class Thing(models.Model): hashid = HashidsField( verbose_name=_('Hashids'), primary_key=True, hashids_salt="%s:reservations:thing" % settings.HASHIDS_SOLT, hashids_min_length=8, ) name = models.CharField(_('Name'), max_length=255) remarks = models.TextField(_("Remarks"), blank=True) thumbnail = ThumbnailField( _("Thumbnail"), upload_to="reservations/thing/thumbnails", blank=True, ) owner = models.ForeignKey( settings.AUTH_USER_MODEL, verbose_name=_("Owner"), related_name="things", editable=False, ) ipaddress = models.GenericIPAddressField( _("IPAddress"), editable=False, blank=True, null=True, ) created_at = models.DateTimeField(_("Created at"), auto_now_add=True) updated_at = models.DateTimeField(_("Modified at"), auto_now=True) class Meta: verbose_name = _("Thing") verbose_name_plural = _("Things") def __str__(self): return self.name def get_absolute_url(self): return reverse('reservations:things-detail', kwargs=dict(pk=self.pk, ))
class Project(models.Model): """ 現在進行形で作成しているプロジェクトを示すモデル メンバーであれば自由に作成可能で所有者および参加者が編集権限を持つ また削除権限は所有者のみが持ち、所有権限の委託は未だ作成されていない。 """ def _get_upload_path(self, filename): basedir = os.path.join('projects', self.slug) return os.path.join(basedir, filename) STATUS = ( ("planning", _("Planning")), ("active", _("Active")), ("paused", _("Suspended")), ("eternal", _("Eternaled")), ("done", _("Released")), ) # 必須フィールド pub_state = models.CharField(_("Publish status"), max_length=10, choices=PUB_STATES, default="public") status = models.CharField(_("Status"), default="planning", max_length=15, choices=STATUS) title = models.CharField(_('Title'), max_length=127, unique=True) slug = models.SlugField(_('Project slug'), unique=True, max_length=63, help_text=_("It will be used on the url of the " "project thus it only allow " "alphabetical or numeric " "characters, underbar ('_'), and " "hyphen ('-'). " "Additionally this value cannot be " "modified for preventing the URL " "changes.")) body = models.TextField(_('Description')) # 省略可能フィールド icon = ThumbnailField(_('Thumbnail'), upload_to=_get_upload_path, blank=True, patterns=settings.THUMBNAIL_SIZE_PATTERNS) category = models.ForeignKey(Category, verbose_name=_('Category'), null=True, blank=True, related_name='projects', help_text=_("Contact us if you cannot find " "a category you need.")) # 自動/API設定 administrator = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Administrator'), related_name="projects_owned", editable=False) members = models.ManyToManyField(settings.AUTH_USER_MODEL, verbose_name=_('Members'), related_name="projects_joined", editable=False) created_at = models.DateTimeField(_('Created at'), auto_now_add=True) updated_at = models.DateTimeField(_('Updated at'), auto_now=True) last_modifier = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Last modified by'), editable=False, null=True, related_name='last_modified_projects') tracker = models.URLField(_('Tracker URL'), blank=True, default='', help_text='Kawaz RedmineのプロジェクトURLを入力してください') repository = models.URLField(_('Repository URL'), blank=True, default='', help_text='Kawaz GitLab, GitHubなどのプロジェクトURLを入力してください') objects = ProjectManager() class Meta: ordering = ('status', '-updated_at', 'title') verbose_name = _('Project') verbose_name_plural = _('Projects') permissions = ( ('join_project', 'Can join to the project'), ('quit_project', 'Can quit from the project'), ('view_project', 'Can view the project'), ) def __str__(self): return self.title def join(self, user): """ 指定されたユーザーを参加させる ユーザーに参加権限がない場合は `PermissionDenied` を投げる """ if not user.has_perm('projects.join_project', self): raise PermissionDenied self.members.add(user) def quit(self, user): """ 指定されたユーザーを退会させる ユーザーに退会権限がない場合は `PermissionDenied` を投げる """ if not user.has_perm('projects.quit_project', self): raise PermissionDenied self.members.remove(user) # TODO: Persona.is_member と若干かぶるため名前を変えたほうが良い def is_member(self, user): """ 指定されたユーザーがこのプロジェクトに参加しているか否か """ return user in self.members.all() @models.permalink def get_absolute_url(self): if self.pub_state == 'draft': return ('projects_project_update', (), { 'pk': self.pk }) return ('projects_project_detail', (), { 'slug': self.slug }) def get_default_icon(self, size): """ デフォルトアイコンを返します """ filename = 'project_icon_{}.png'.format(size) return os.path.join('/statics', 'img', 'defaults', filename) def get_icon(self, size): """ 渡したサイズのアイコンURLを返します 未設定の場合や、見つからない場合はデフォルトアイコンを返します """ if self.icon: try: return getattr(self.icon, size).url except: return self.get_default_icon(size) return self.get_default_icon(size) @property def active_members(self): return self.members.filter(is_active=True) @property def is_legacy(self): """ 企画中か活動中、かつ180日以上更新されていない場合、Trueが返ります """ import datetime from django.utils import timezone now = timezone.now() past = now - datetime.timedelta(days=180) return self.status in ['planning', 'active',] and self.updated_at < past get_small_icon = lambda self: self.get_icon('small') get_middle_icon = lambda self: self.get_icon('middle') get_large_icon = lambda self: self.get_icon('large') get_huge_icon = lambda self: self.get_icon('huge')
class Product(models.Model): """ 完成したプロダクトを表すモデル メンバーであれば誰でも作成・管理可能 """ def _get_advertisement_image_upload_path(self, filename): basedir = os.path.join('products', self.slug, 'advertisement_images') return os.path.join(basedir, filename) def _get_thumbnail_upload_path(self, filename): basedir = os.path.join('products', self.slug, 'thumbnails') return os.path.join(basedir, filename) DISPLAY_MODES = ( ('featured', _('Fetured: Displayed in the curled cell and the tiled cell ' 'on the top page')), ('tiled', _('Tiled: Displayed in the tiled cell on the top page')), ('normal', _('Normal: Displayed only in tiled cell on the detailed page')), ) # 必須フィールド title = models.CharField(_('Title'), max_length=128, unique=True) slug = models.SlugField(_('Product slug'), unique=True, help_text=_("It will be used on the url of the " "product thus it only allow " "alphabetical or numeric " "characters, underbar ('_'), and " "hyphen ('-'). " "Additionally this value cannot be " "modified for preventing the URL " "changes.")) thumbnail = ThumbnailField( _('Thumbnail'), upload_to=_get_thumbnail_upload_path, patterns=PRODUCT_THUMBNAIL_SIZE_PATTERNS, help_text=_("This would be used as a product thumbnail image. " "The aspect ratio of the image should be 16:9." "We recommend the image size to be 800 * 450.")) description = models.TextField(_('Description'), max_length=4096) # 省略可能フィールド advertisement_image = ThumbnailField( _('Advertisement Image'), null=True, blank=True, upload_to=_get_advertisement_image_upload_path, patterns=ADVERTISEMENT_IMAGE_SIZE_PATTERNS, help_text=_("This would be used in the top page. " "The aspect ratio of the image should be 16:9" "We recommend the image size to be 800 * 450")) trailer = models.URLField( _('Trailer'), null=True, blank=True, help_text=_("Enter URL of your trailer movie on the YouTube. " "The movie would be embeded to the product page.")) project = models.OneToOneField(Project, verbose_name=_('Project'), null=True, blank=True, related_name='product') platforms = models.ManyToManyField(Platform, verbose_name=_('Platforms'), related_name='products') categories = models.ManyToManyField(Category, verbose_name=_('Categories')) contact_info = models.CharField( _('Contact info'), default='', blank=True, max_length=256, help_text=_("Fill your contact info for visitors, " "e.g. Twitter account, Email address or Facebook account")) published_at = models.DateField( _('Published at'), help_text=_("If this product have been already released, " "please fill the date.")) administrators = models.ManyToManyField(Persona, verbose_name=_('Administrators')) created_at = models.DateTimeField(_('Created at'), auto_now_add=True) updated_at = models.DateTimeField(_('Updated at'), auto_now=True) last_modifier = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=_('Last modified by'), editable=False, null=True, related_name='last_modified_products') # # Productの表示順番を制御する値です。Formsでexclude設定されるため通常 # ユーザーからは変更できません(adminサイトでの修正) # # featured: トップページのカルーセル + トップページにタイル表示されます。 # 設定するにはadvertisement_imageの設定が必要です # tiled : トップページにタイル表示されます。 # タイル表示は thumbnails が使われます # normal : トップページには表示されず、see more内でのみタイル表示されます # display_mode = models.CharField( _('Display mode'), max_length=10, choices=DISPLAY_MODES, default='normal', help_text=_( "How the product displayed on the top page. " "To use 'Featured', an 'Advertisement image' is required.")) class Meta: ordering = ( 'display_mode', '-published_at', ) verbose_name = _('Product') verbose_name_plural = _('Products') permissions = ( ('join_product', 'Can join to the product'), ('quit_product', 'Can quit from the product'), ) def __str__(self): return self.title def clean(self): if not self.advertisement_image and self.display_mode == 'featured': # advertisement_imageがセットされていないときは # display_modeをfeaturedに設定できない raise ValidationError( _("Display mode 'Feature' requires 'Advertisement image'")) if self.slug in INVALID_PRODUCT_SLUGS: raise ValidationError( _("A product slug '%s' is reserved.") % self.slug) def join(self, user): """ 指定されたユーザーを管理者にする ユーザーに参加権限がない場合は `PermissionDenied` を投げる """ if not user.has_perm('products.join_product', self): raise PermissionDenied self.administrators.add(user) def quit(self, user): """ 指定されたユーザーを管理者から外す ユーザーに脱退権限がない場合は `PermissionDenied` を投げる """ if not user.has_perm('products.quit_product', self): raise PermissionDenied self.administrators.remove(user) @models.permalink def get_absolute_url(self): return ('products_product_detail', (), {'slug': self.slug})
class Persona(AbstractUser, metaclass=PersonaBase): """ Kawazで利用する認証用カスタムユーザーモデル このユーザーモデルはDjangoデフォルトのモデルと異なり`is_staff`と `is_superuser`の値をDB上に保持しない。代わりにこれらの値は`role`の値から 自動的に決定され、プロパティとして提供される """ def _get_upload_path(self, filename): root = os.path.join('personas', 'avatars', self.username) return os.path.join(root, filename) GENDER_TYPES = (('man', _("Man")), ('woman', _("Woman")), ('unknown', _("Unknown"))) ROLE_TYPES = ( ('adam', _('Adam')), # superusers ('seele', _('Seele')), # admin ('nerv', _('Nerv')), # staff ('children', _('Children')), # Kawaz members ('wille', _('Wille')), # external users ) nickname = models.CharField(_('Nickname'), max_length=30) quotes = models.CharField(_('Mood message'), max_length=127, blank=True) avatar = ThumbnailField(_('Avatar'), upload_to=_get_upload_path, blank=True, patterns=settings.THUMBNAIL_SIZE_PATTERNS) gender = models.CharField(_('Gender'), max_length=10, choices=GENDER_TYPES, default='unknown') role = models.CharField(_('Role'), max_length=10, choices=ROLE_TYPES, default='wille', help_text=_( "The role this user belongs to. " "A user will get permissions of the role thus " "the user cannot change ones role for " "security reason.")) objects = PersonaManager() actives = ActivePersonaManager() class Meta: ordering = ('username', ) verbose_name = _('Persona') verbose_name_plural = _('Personas') permissions = ( ('activate_persona', 'Can activate/deactivate the persona'), ('assign_role_persona', 'Can assign the role to the persona'), ) @property def is_staff(self): return self.role in ('adam', 'seele', 'nerv') @property def is_superuser(self): return self.role in ('adam', ) @property def is_member(self): return self.role in ('adam', 'seele', 'nerv', 'children') def get_default_avatar(self, size): """ デフォルトアバターを返します """ filename = 'persona_avatar_{}.png'.format(size) return os.path.join('/statics', 'img', 'defaults', filename) def get_avatar(self, size): """ 渡したサイズのアバターURLを返します 未設定の場合や、見つからない場合はデフォルトアバターを返します """ if self.avatar: try: return getattr(self.avatar, size).url except: return self.get_default_avatar(size) return self.get_default_avatar(size) get_small_avatar = lambda self: self.get_avatar('small') get_middle_avatar = lambda self: self.get_avatar('middle') get_large_avatar = lambda self: self.get_avatar('large') get_huge_avatar = lambda self: self.get_avatar('huge') def clean_fields(self, exclude=None, **kwargs): # 使用不可な文字列が指定されていた場合はエラー # Note: # AbstractUser では RegexValidator('^[\\w.@+-]+$') # でチェックしているが Kawaz の仕様的に . @ + は使用できると不味い # ので追加でチェックしている if not VALID_USERNAME_PATTERN.match(self.username): raise ValidationError( _("The username '%(username)s' contains invalid characters. " "Letters, digits, and - are the only characters available.") % {'username': self.username}) # 使用不可のユーザー名が指定されていた場合はエラー if self.username in INVALID_USERNAMES: raise ValidationError( _("The username '%(username)s' is reserved. " "Please chose a different username.") % {'username': self.username}) # ニックネームが指定されていない場合は自動的にユーザー名を当てはめる if not self.nickname: self.nickname = self.username super().clean_fields(exclude=exclude, **kwargs) @models.permalink def get_absolute_url(self): return ('personas_persona_detail', (), {'slug': self.username})